Compare commits
544 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
007b518503 | ||
|
|
c6edc620c8 | ||
|
|
3f9d7c9cc0 | ||
|
|
6c1845e246 | ||
|
|
0b2572a730 | ||
|
|
6fc46d632b | ||
|
|
b2a35813f6 | ||
|
|
279b76ad53 | ||
|
|
b550016681 | ||
|
|
3ab5d12f3e | ||
|
|
53bcb8b85f | ||
|
|
94397dcb4c | ||
|
|
53fbee2d44 | ||
|
|
e46ad3595d | ||
|
|
f2b03b6e69 | ||
|
|
0df2a6bdc0 | ||
|
|
df31e5ccd1 | ||
|
|
4dfa0dada6 | ||
|
|
857a523f84 | ||
|
|
ceac838706 | ||
|
|
e8cb6d27fc | ||
|
|
78428f6bd3 | ||
|
|
da94dfba70 | ||
|
|
8275792f45 | ||
|
|
a90923011a | ||
|
|
1aa24e2871 | ||
|
|
68ce751745 | ||
|
|
74ff64d41a |
95
.github/ISSUE_TEMPLATE/bug-issue.yml
vendored
@@ -1,5 +1,5 @@
|
||||
name: 🐞 Bug report
|
||||
description: Report a very clearly broken issue.
|
||||
description: Create a new bug report.
|
||||
title: 'bug: <title>'
|
||||
labels: [bug]
|
||||
body:
|
||||
@@ -8,53 +8,20 @@ body:
|
||||
value: |
|
||||
# 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).
|
||||
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Type
|
||||
options:
|
||||
- Error while running the manager
|
||||
- Error at runtime
|
||||
- Cosmetic
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
Please check for existing issues [here](https://github.com/revanced/revanced-manager/labels/bug) before creating a new one.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Bug description
|
||||
description: 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:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Steps to reproduce
|
||||
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.
|
||||
label: Version of ReVanced Manager and version & name of application you tried to patch
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
@@ -64,57 +31,31 @@ body:
|
||||
- Non-root
|
||||
- Root
|
||||
validations:
|
||||
required: true
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Patches selected.
|
||||
description: Patches you selected for the app.
|
||||
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.
|
||||
label: Device logs
|
||||
description: Export logs in ReVanced Manager settings.
|
||||
render: shell
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Installer logs (exported using Installer menu option) [unneeded if issue is not during patching].
|
||||
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
|
||||
label: Patcher logs
|
||||
description: Export logs in "Patcher" screen.
|
||||
render: shell
|
||||
validations:
|
||||
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
|
||||
id: acknowledgements
|
||||
attributes:
|
||||
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:
|
||||
- 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
|
||||
- label: I have written a short but informative title.
|
||||
- label: I have chosen an appropriate title.
|
||||
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
|
||||
- label: The issue is related solely to the ReVanced Manager
|
||||
- label: The issue is solely related to the ReVanced Manager
|
||||
required: true
|
||||
|
||||
40
.github/ISSUE_TEMPLATE/feature-issue.yml
vendored
@@ -1,52 +1,42 @@
|
||||
name: ⭐ Feature request
|
||||
description: Create a detailed feature request.
|
||||
description: Create a new feature request.
|
||||
title: 'feat: <title>'
|
||||
labels: [feature-request]
|
||||
body:
|
||||
- type: dropdown
|
||||
- type: markdown
|
||||
attributes:
|
||||
label: Type
|
||||
options:
|
||||
- Functionality
|
||||
- Cosmetic
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
value: |
|
||||
# ReVanced Manager feature request
|
||||
|
||||
Please check for existing feature requests [here](https://github.com/revanced/revanced-manager/labels/bug) before creating a new one.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Issue
|
||||
description: What is the current problem. Why does it require a feature request?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Feature
|
||||
description: Describe your feature in detail. How does it solve the issue?
|
||||
label: Feature description
|
||||
description: Describe your feature in detail.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Motivation
|
||||
description: Why should your feature should be considered?
|
||||
description: Explain why the lack of it is a problem.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add additional context here.
|
||||
description: In case there is something else you want to add.
|
||||
validations:
|
||||
required: false
|
||||
- type: checkboxes
|
||||
id: acknowledgements
|
||||
attributes:
|
||||
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:
|
||||
- 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
|
||||
- label: I have written a short but informative title.
|
||||
- label: I have chosen an appropriate title.
|
||||
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
|
||||
- label: The issue is related solely to the ReVanced Manager
|
||||
- label: The issue is solely related to the ReVanced Manager
|
||||
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
|
||||
45
.github/workflows/pr-build.yml
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
name: PR Build
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- ".github/workflows/pr-build.yml"
|
||||
- "android/**"
|
||||
- "assets/**"
|
||||
- "lib/**"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
# Make sure the release step uses its own credentials:
|
||||
# https://github.com/cycjimmy/semantic-release-action#private-packages
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
- name: Setup JDK
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: '11'
|
||||
distribution: 'zulu'
|
||||
- name: Setup Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: 'stable'
|
||||
cache: true
|
||||
- name: Install Flutter dependencies
|
||||
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: Upload build
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: revanced-manager
|
||||
path: build/app/outputs/flutter-apk/app-debug.apk
|
||||
25
.github/workflows/release-build.yml
vendored
@@ -9,16 +9,17 @@ jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
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 12
|
||||
uses: actions/setup-java@v1
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: '12.x'
|
||||
- uses: subosito/flutter-action@v1
|
||||
java-version: "11"
|
||||
distribution: "zulu"
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: 'stable'
|
||||
channel: "stable"
|
||||
- name: Set up Flutter
|
||||
run: flutter pub get
|
||||
- name: Generate files with Builder
|
||||
@@ -32,18 +33,18 @@ jobs:
|
||||
run: flutter build apk
|
||||
- name: Sign APK
|
||||
id: sign_apk
|
||||
uses: r0adkll/sign-android-release@v1
|
||||
uses: ilharp/sign-android-release@v1
|
||||
with:
|
||||
releaseDirectory: build/app/outputs/apk/release
|
||||
signingKeyBase64: ${{ secrets.SIGNING_KEYSTORE }}
|
||||
releaseDir: build/app/outputs/apk/release
|
||||
signingKey: ${{ secrets.SIGNING_KEYSTORE }}
|
||||
keyStorePassword: ${{ secrets.SIGNING_KEYSTORE_PASSWORD }}
|
||||
alias: ${{ secrets.SIGNING_KEY_ALIAS }}
|
||||
keyAlias: ${{ secrets.SIGNING_KEY_ALIAS }}
|
||||
keyPassword: ${{ secrets.SIGNING_KEY_PASSWORD }}
|
||||
- name: Add version to APK
|
||||
run: mv ${{steps.sign_apk.outputs.signedReleaseFile}} revanced-manager-${{ env.RELEASE_VERSION }}.apk
|
||||
run: mv ${{steps.sign_apk.outputs.signedFile}} revanced-manager-${{ env.RELEASE_VERSION }}.apk
|
||||
- name: Publish release APK
|
||||
uses: "marvinpinto/action-automatic-releases@latest"
|
||||
with:
|
||||
repo_token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
prerelease: false
|
||||
files: revanced-manager-${{ env.RELEASE_VERSION }}.apk
|
||||
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 }}"}'
|
||||
9
.gitignore
vendored
@@ -58,6 +58,7 @@ unlinked.ds
|
||||
unlinked_spec.ds
|
||||
|
||||
# Android related
|
||||
.gradle/
|
||||
**/android/**/gradle-wrapper.jar
|
||||
**/android/.gradle
|
||||
**/android/captures/
|
||||
@@ -134,5 +135,11 @@ app.*.map.json
|
||||
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
|
||||
!/dev/ci/**/Gemfile.lock
|
||||
|
||||
Firebase related
|
||||
# Firebase related
|
||||
.firebase
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
|
||||
# FVM
|
||||
.fvm
|
||||
6
.run/main.dart.run.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<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>
|
||||
35
README.md
@@ -3,18 +3,33 @@
|
||||
The official ReVanced Manager based on Flutter.
|
||||
|
||||
## 🔽 Download
|
||||
To download the Alpha version of Manager, go [here](https://github.com/revanced/revanced-manager/releases/latest) and install the provided APK file.
|
||||
|
||||
You can obtain ReVanced Manager by downloading it from either [revanced.app/download](https://revanced.app/download) or [GitHub Releases](https://github.com/ReVanced/revanced-manager/releases)
|
||||
|
||||
## 📝 Prerequisites
|
||||
|
||||
1. Android 8 or higher
|
||||
2. Incompatible with certain ARMv7 devices
|
||||
|
||||
## 📃 Documentation
|
||||
The documentation can be found [here](https://github.com/revanced/revanced-manager/tree/main/docs).
|
||||
|
||||
## 🔴 Issues
|
||||
For suggestions and bug reports, open an issue [here](https://github.com/revanced/revanced-manager/issues/new/choose).
|
||||
|
||||
## 💭 Discussion
|
||||
If you wish to discuss the Manager, a thread has been made under the [#chat](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.
|
||||
For suggestions and bug reports, open an issue [here](https://github.com/revanced/revanced-manager/issues/new/choose).
|
||||
|
||||
## ⚠️ Disclaimer
|
||||
*Please note that even though we're releasing the Manager, it is an ALPHA version. Meaning there's a big chance that the Manager might not work at all for you.*
|
||||
## 🌐 Translation
|
||||
|
||||
## Prerequisites
|
||||
1. Android 8 or higher.
|
||||
2. Does not work on armv7
|
||||
3. For YouTube and YouTube Music - Vanced MicroG(Only for non-root).
|
||||
[](https://crowdin.com/project/revanced)
|
||||
|
||||
We're accepting translations on [Crowdin](https://translate.revanced.app).
|
||||
|
||||
## 🛠️ Building Manager from source
|
||||
|
||||
1. Setup flutter environment for your [platform](https://docs.flutter.dev/get-started/install)
|
||||
2. Clone the repository locally
|
||||
3. Add your GitHub token in gradle.properties like [this](/docs/4_building.md)
|
||||
4. Open the project in terminal
|
||||
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)
|
||||
7. To build release APK run `flutter build apk`
|
||||
|
||||
@@ -9,21 +9,155 @@
|
||||
# packages, and plugins designed to encourage good coding practices.
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
|
||||
linter:
|
||||
# The lint rules applied to this project can be customized in the
|
||||
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
||||
# included above or to enable additional rules. A list of all available lints
|
||||
# and their documentation is published at
|
||||
# https://dart-lang.github.io/linter/lints/index.html.
|
||||
#
|
||||
# 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
|
||||
analyzer:
|
||||
exclude:
|
||||
- lib/app/app.locator.dart
|
||||
- lib/app/app.router.dart
|
||||
- lib/models/patch.g.dart
|
||||
- lib/models/patched_application.g.dart
|
||||
|
||||
# Additional information about this file can be found at
|
||||
# https://dart.dev/guides/language/analysis-options
|
||||
linter:
|
||||
rules:
|
||||
- always_declare_return_types
|
||||
- require_trailing_commas
|
||||
- always_put_control_body_on_new_line
|
||||
- always_require_non_null_named_parameters
|
||||
- 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
|
||||
- avoid_returning_null_for_future
|
||||
- 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_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"
|
||||
@@ -26,7 +26,7 @@ apply plugin: 'kotlin-android'
|
||||
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
|
||||
|
||||
android {
|
||||
compileSdkVersion 33
|
||||
compileSdkVersion flutter.compileSdkVersion
|
||||
ndkVersion flutter.ndkVersion
|
||||
|
||||
compileOptions {
|
||||
@@ -54,7 +54,21 @@ android {
|
||||
release {
|
||||
shrinkResources false
|
||||
minifyEnabled false
|
||||
resValue "string", "app_name", "ReVanced Manager"
|
||||
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'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +85,7 @@ dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
|
||||
// ReVanced
|
||||
implementation "app.revanced:revanced-patcher:5.1.1"
|
||||
implementation "app.revanced:revanced-patcher:17.0.0"
|
||||
|
||||
// Signing & aligning
|
||||
implementation("org.bouncycastle:bcpkix-jdk15on:1.70")
|
||||
|
||||
@@ -2,21 +2,29 @@
|
||||
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.READ_MEDIA_IMAGES" />
|
||||
<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_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_INSTALL_PACKAGES" />
|
||||
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
|
||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
|
||||
<application
|
||||
android:label="ReVanced Manager"
|
||||
android:label="@string/app_name"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:largeHeap="true"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:extractNativeLibs="true">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
@@ -28,13 +36,16 @@
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme"
|
||||
/>
|
||||
android:resource="@style/NormalTheme"/>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ExportSettingsActivity"
|
||||
android:exported="true">
|
||||
</activity>
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
|
||||
@@ -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,77 +1,161 @@
|
||||
package app.revanced.manager.flutter
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.annotation.NonNull
|
||||
import app.revanced.manager.flutter.utils.Aapt
|
||||
import app.revanced.manager.flutter.utils.aligning.ZipAligner
|
||||
import app.revanced.manager.flutter.utils.signing.Signer
|
||||
import app.revanced.manager.flutter.utils.zip.ZipFile
|
||||
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.PatcherOptions
|
||||
import app.revanced.patcher.extensions.PatchExtensions.patchName
|
||||
import app.revanced.patcher.logging.Logger
|
||||
import app.revanced.patcher.util.patch.impl.DexPatchBundle
|
||||
import dalvik.system.DexClassLoader
|
||||
import app.revanced.patcher.patch.PatchResult
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import java.io.File
|
||||
|
||||
private const val PATCHER_CHANNEL = "app.revanced.manager.flutter/patcher"
|
||||
private const val INSTALLER_CHANNEL = "app.revanced.manager.flutter/installer"
|
||||
import java.io.PrintWriter
|
||||
import java.io.StringWriter
|
||||
import java.util.logging.LogRecord
|
||||
import java.util.logging.Logger
|
||||
|
||||
class MainActivity : FlutterActivity() {
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
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)
|
||||
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 mainChannel =
|
||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, patcherChannel)
|
||||
|
||||
this.installerChannel =
|
||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, installerChannel)
|
||||
|
||||
mainChannel.setMethodCallHandler { call, result ->
|
||||
when (call.method) {
|
||||
"runPatcher" -> {
|
||||
val patchBundleFilePath = call.argument<String>("patchBundleFilePath")
|
||||
val originalFilePath = call.argument<String>("originalFilePath")
|
||||
val inputFilePath = call.argument<String>("inputFilePath")
|
||||
val patchedFilePath = call.argument<String>("patchedFilePath")
|
||||
val outFilePath = call.argument<String>("outFilePath")
|
||||
val integrationsPath = call.argument<String>("integrationsPath")
|
||||
val selectedPatches = call.argument<List<String>>("selectedPatches")
|
||||
val options = call.argument<Map<String, Map<String, Any>>>("options")
|
||||
val cacheDirPath = call.argument<String>("cacheDirPath")
|
||||
val mergeIntegrations = call.argument<Boolean>("mergeIntegrations")
|
||||
val keyStoreFilePath = call.argument<String>("keyStoreFilePath")
|
||||
if (patchBundleFilePath != null &&
|
||||
val keystorePassword = call.argument<String>("keystorePassword")
|
||||
|
||||
if (
|
||||
originalFilePath != null &&
|
||||
inputFilePath != null &&
|
||||
patchedFilePath != null &&
|
||||
outFilePath != null &&
|
||||
integrationsPath != null &&
|
||||
selectedPatches != null &&
|
||||
options != null &&
|
||||
cacheDirPath != null &&
|
||||
mergeIntegrations != null &&
|
||||
keyStoreFilePath != null
|
||||
keyStoreFilePath != null &&
|
||||
keystorePassword != null
|
||||
) {
|
||||
cancel = false
|
||||
runPatcher(
|
||||
result,
|
||||
patchBundleFilePath,
|
||||
originalFilePath,
|
||||
inputFilePath,
|
||||
patchedFilePath,
|
||||
outFilePath,
|
||||
integrationsPath,
|
||||
selectedPatches,
|
||||
options,
|
||||
cacheDirPath,
|
||||
mergeIntegrations,
|
||||
keyStoreFilePath
|
||||
keyStoreFilePath,
|
||||
keystorePassword
|
||||
)
|
||||
} else {
|
||||
result.notImplemented()
|
||||
}
|
||||
} else result.notImplemented()
|
||||
}
|
||||
|
||||
"stopPatcher" -> {
|
||||
cancel = true
|
||||
stopResult = result
|
||||
}
|
||||
|
||||
"getPatches" -> {
|
||||
val patchBundleFilePath = call.argument<String>("patchBundleFilePath")!!
|
||||
val cacheDirPath = call.argument<String>("cacheDirPath")!!
|
||||
|
||||
try {
|
||||
patches = PatchBundleLoader.Dex(
|
||||
File(patchBundleFilePath),
|
||||
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 ->
|
||||
val optionJson = JSONObject().apply option@{
|
||||
put("key", option.key)
|
||||
put("title", option.title)
|
||||
put("description", option.description)
|
||||
put("required", option.required)
|
||||
|
||||
when (val value = option.value) {
|
||||
null -> put("value", null)
|
||||
is Array<*> -> put("value", JSONArray().apply {
|
||||
|
||||
value.forEach { put(it) }
|
||||
})
|
||||
else -> put("value", option.value)
|
||||
}
|
||||
|
||||
put("optionClassType", option::class.simpleName)
|
||||
}
|
||||
put(optionJson)
|
||||
}
|
||||
})
|
||||
}.let(::put)
|
||||
}
|
||||
}.toString().let(result::success)
|
||||
}
|
||||
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
@@ -79,16 +163,16 @@ class MainActivity : FlutterActivity() {
|
||||
|
||||
private fun runPatcher(
|
||||
result: MethodChannel.Result,
|
||||
patchBundleFilePath: String,
|
||||
originalFilePath: String,
|
||||
inputFilePath: String,
|
||||
patchedFilePath: String,
|
||||
outFilePath: String,
|
||||
integrationsPath: String,
|
||||
selectedPatches: List<String>,
|
||||
options: Map<String, Map<String, Any>>,
|
||||
cacheDirPath: String,
|
||||
mergeIntegrations: Boolean,
|
||||
keyStoreFilePath: String
|
||||
keyStoreFilePath: String,
|
||||
keystorePassword: String
|
||||
) {
|
||||
val originalFile = File(originalFilePath)
|
||||
val inputFile = File(inputFilePath)
|
||||
@@ -96,125 +180,148 @@ class MainActivity : FlutterActivity() {
|
||||
val outFile = File(outFilePath)
|
||||
val integrations = File(integrationsPath)
|
||||
val keyStoreFile = File(keyStoreFilePath)
|
||||
val cacheDir = File(cacheDirPath)
|
||||
|
||||
Thread {
|
||||
try {
|
||||
val patches = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.CUPCAKE) {
|
||||
DexPatchBundle(
|
||||
patchBundleFilePath,
|
||||
DexClassLoader(
|
||||
patchBundleFilePath,
|
||||
cacheDirPath,
|
||||
null,
|
||||
javaClass.classLoader
|
||||
)
|
||||
).loadPatches().filter { patch -> selectedPatches.any { it == patch.patchName } }
|
||||
} else {
|
||||
TODO("VERSION.SDK_INT < CUPCAKE")
|
||||
}
|
||||
|
||||
fun updateProgress(progress: Double, header: String, log: String) {
|
||||
handler.post {
|
||||
installerChannel.invokeMethod(
|
||||
"update",
|
||||
mapOf(
|
||||
"progress" to 0.1,
|
||||
"header" to "",
|
||||
"log" to "Copying original apk"
|
||||
"progress" to progress,
|
||||
"header" to header,
|
||||
"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)
|
||||
|
||||
handler.post {
|
||||
installerChannel.invokeMethod(
|
||||
"update",
|
||||
mapOf(
|
||||
"progress" to 0.2,
|
||||
"header" to "Unpacking apk...",
|
||||
"log" to "Unpacking input apk"
|
||||
)
|
||||
)
|
||||
if (cancel) {
|
||||
postStop()
|
||||
return@Thread
|
||||
}
|
||||
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 "")
|
||||
updateProgress(0.05, "Reading APK...", "Reading APK")
|
||||
|
||||
val patcher = Patcher(
|
||||
PatcherOptions(
|
||||
inputFile,
|
||||
cacheDir,
|
||||
Aapt.binary(applicationContext).absolutePath,
|
||||
cacheDir.path,
|
||||
true // TODO: Add option to disable this
|
||||
)
|
||||
)
|
||||
|
||||
if (cancel) {
|
||||
postStop()
|
||||
return@Thread
|
||||
}
|
||||
if (mergeIntegrations) {
|
||||
handler.post {
|
||||
installerChannel.invokeMethod(
|
||||
"update",
|
||||
mapOf(
|
||||
"progress" to 0.4,
|
||||
"header" to "Merging integrations...",
|
||||
"log" to "Merging integrations"
|
||||
)
|
||||
)
|
||||
|
||||
updateProgress(0.1, "Loading patches...", "Loading patches")
|
||||
|
||||
val patches = patches.filter { patch ->
|
||||
val isCompatible = patch.compatiblePackages?.any {
|
||||
it.name == patcher.context.packageMetadata.packageName
|
||||
} ?: false
|
||||
|
||||
val compatibleOrUniversal =
|
||||
isCompatible || patch.compatiblePackages.isNullOrEmpty()
|
||||
|
||||
compatibleOrUniversal && selectedPatches.any { it == patch.name }
|
||||
}.onEach { patch ->
|
||||
options[patch.name]?.forEach { (key, value) ->
|
||||
patch.options[key] = value
|
||||
}
|
||||
patcher.addFiles(listOf(integrations)) {}
|
||||
}
|
||||
|
||||
handler.post {
|
||||
installerChannel.invokeMethod(
|
||||
"update",
|
||||
mapOf(
|
||||
"progress" to 0.5,
|
||||
"header" to "Applying patches...",
|
||||
"log" to ""
|
||||
)
|
||||
)
|
||||
if (cancel) {
|
||||
postStop()
|
||||
return@Thread
|
||||
}
|
||||
|
||||
patcher.addPatches(patches)
|
||||
patcher.applyPatches().forEach { (patch, res) ->
|
||||
if (res.isSuccess) {
|
||||
val msg = "[success] $patch"
|
||||
handler.post {
|
||||
installerChannel.invokeMethod(
|
||||
"update",
|
||||
mapOf(
|
||||
"progress" to 0.5,
|
||||
"header" to "",
|
||||
"log" to msg
|
||||
)
|
||||
)
|
||||
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 { patchResult: PatchResult ->
|
||||
if (cancel) {
|
||||
handler.post { stopResult!!.success(null) }
|
||||
this.cancel()
|
||||
this@apply.close()
|
||||
return@collect
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
return@forEach
|
||||
}
|
||||
val msg = "[error] $patch:" + res.exceptionOrNull()!!
|
||||
handler.post {
|
||||
installerChannel.invokeMethod(
|
||||
"update",
|
||||
mapOf("progress" to 0.5, "header" to "", "log" to msg)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
handler.post {
|
||||
installerChannel.invokeMethod(
|
||||
"update",
|
||||
mapOf(
|
||||
"progress" to 0.7,
|
||||
"header" to "Repacking apk...",
|
||||
"log" to "Repacking patched apk"
|
||||
)
|
||||
)
|
||||
if (cancel) {
|
||||
postStop()
|
||||
patcher.close()
|
||||
return@Thread
|
||||
}
|
||||
val res = patcher.save()
|
||||
|
||||
updateProgress(0.8, "Building...", "")
|
||||
|
||||
val res = patcher.get()
|
||||
patcher.close()
|
||||
|
||||
ZipFile(patchedFile).use { file ->
|
||||
res.dexFiles.forEach {
|
||||
if (cancel) {
|
||||
postStop()
|
||||
return@Thread
|
||||
}
|
||||
file.addEntryCompressData(
|
||||
ZipEntry.createWithName(it.name),
|
||||
it.stream.readBytes()
|
||||
@@ -231,81 +338,35 @@ class MainActivity : FlutterActivity() {
|
||||
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 {
|
||||
installerChannel.invokeMethod(
|
||||
"update",
|
||||
mapOf(
|
||||
"progress" to 1.0,
|
||||
"header" to "Finished!",
|
||||
"log" to "Finished!"
|
||||
)
|
||||
)
|
||||
if (cancel) {
|
||||
postStop()
|
||||
return@Thread
|
||||
}
|
||||
|
||||
updateProgress(0.9, "Signing...", "Signing APK")
|
||||
|
||||
try {
|
||||
Signer("ReVanced", keystorePassword)
|
||||
.signApk(patchedFile, outFile, keyStoreFile)
|
||||
} catch (e: Exception) {
|
||||
print("Error signing APK: ${e.message}")
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
updateProgress(1.0, "Patched", "Patched")
|
||||
} catch (ex: Throwable) {
|
||||
val stack = ex.stackTraceToString()
|
||||
handler.post {
|
||||
installerChannel.invokeMethod(
|
||||
"update",
|
||||
mapOf(
|
||||
"progress" to -100.0,
|
||||
"header" to "Aborting...",
|
||||
"log" to "An error occurred! Aborting\nError:\n$stack"
|
||||
)
|
||||
if (!cancel) {
|
||||
val stack = ex.stackTraceToString()
|
||||
updateProgress(
|
||||
-100.0,
|
||||
"Failed",
|
||||
"An error occurred:\n$stack"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
handler.post { result.success(null) }
|
||||
}.start()
|
||||
}
|
||||
|
||||
inner class ManagerLogger : Logger {
|
||||
override fun error(msg: String) {
|
||||
handler.post {
|
||||
installerChannel
|
||||
.invokeMethod(
|
||||
"update",
|
||||
mapOf("progress" to -1.0, "header" to "", "log" to msg)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun warn(msg: String) {
|
||||
handler.post {
|
||||
installerChannel.invokeMethod(
|
||||
"update",
|
||||
mapOf("progress" to -1.0, "header" to "", "log" to msg)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun info(msg: String) {
|
||||
handler.post {
|
||||
installerChannel.invokeMethod(
|
||||
"update",
|
||||
mapOf("progress" to -1.0, "header" to "", "log" to msg)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun trace(msg: String) {
|
||||
handler.post {
|
||||
installerChannel.invokeMethod(
|
||||
"update",
|
||||
mapOf("progress" to -1.0, "header" to "", "log" to msg)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
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"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
<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"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#1B1B1B</color>
|
||||
</resources>
|
||||
</resources>
|
||||
@@ -1,5 +1,5 @@
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.7.10'
|
||||
ext.kotlin_version = '1.9.0'
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
@@ -16,12 +16,9 @@ allprojects {
|
||||
google()
|
||||
mavenCentral()
|
||||
maven {
|
||||
url = uri("https://maven.pkg.github.com/revanced/revanced-patcher")
|
||||
credentials {
|
||||
username = (project.findProperty("gpr.user") ?: System.getenv("GITHUB_ACTOR")) as String
|
||||
password = (project.findProperty("gpr.key") ?: System.getenv("GITHUB_TOKEN")) as String
|
||||
}
|
||||
url 'https://jitpack.io'
|
||||
}
|
||||
mavenLocal()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +28,6 @@ subprojects {
|
||||
project.evaluationDependsOn(':app')
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
tasks.register("clean", Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
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.enableJetifier=true
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#Mon May 09 12:07:41 MSK 2022
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-rc-1-bin.zip
|
||||
distributionPath=wrapper/dists
|
||||
zipStorePath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip
|
||||
|
||||
@@ -1,37 +1,65 @@
|
||||
{
|
||||
"okButton": "OK",
|
||||
"cancelButton": "Cancel",
|
||||
"dismissButton": "Dismiss",
|
||||
"quitButton": "Quit",
|
||||
"updateButton": "Update",
|
||||
"enabledLabel": "Enabled",
|
||||
"disabledLabel": "Disabled",
|
||||
"installed":"Installed: {version}",
|
||||
"suggested":"Suggested: {version}",
|
||||
"yesButton": "Yes",
|
||||
"noButton": "No",
|
||||
"warning": "Warning",
|
||||
"options": "Options",
|
||||
"notice": "Notice",
|
||||
"noShowAgain": "Don't show this again",
|
||||
"add": "Add",
|
||||
"remove": "Remove",
|
||||
"navigationView": {
|
||||
"dashboardTab": "Dashboard",
|
||||
"patcherTab": "Patcher",
|
||||
"settingsTab": "Settings"
|
||||
},
|
||||
"homeView": {
|
||||
"refreshSuccess": "Refreshed successfully",
|
||||
"widgetTitle": "Dashboard",
|
||||
|
||||
"updatesSubtitle": "Updates",
|
||||
"patchedSubtitle": "Patched Applications",
|
||||
"updatesAvailable": "Updates available",
|
||||
"patchedSubtitle": "Patched apps",
|
||||
|
||||
"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",
|
||||
"updateDialogText": "Are you sure you want to download and update ReVanced Manager?",
|
||||
"notificationTitle": "ReVanced Manager was updated",
|
||||
"notificationText": "Tap to open the app",
|
||||
"updatePatchesDialogTitle": "Update ReVanced Patches",
|
||||
"updateChangelogTitle": "Changelog",
|
||||
|
||||
"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...",
|
||||
"downloadedMessage": "Update downloaded!",
|
||||
|
||||
"installingMessage": "Installing update...",
|
||||
|
||||
"errorDownloadMessage": "Unable to download 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": {
|
||||
"patchButton": "Patch",
|
||||
"infoButton": "Info",
|
||||
"changelogLabel": "Changelog"
|
||||
"infoButton": "Info"
|
||||
},
|
||||
"latestCommitCard": {
|
||||
"loadingLabel": "Loading...",
|
||||
@@ -43,21 +71,28 @@
|
||||
"patcherView": {
|
||||
"widgetTitle": "Patcher",
|
||||
"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. Proceed anyways?",
|
||||
|
||||
"removedPatchesWarningDialogText": "The following patches have been removed since the last time you used them.\n\n{patches}\n\nProceed anyways?",
|
||||
"requiredOptionDialogText" : "Some patch options have to be set."
|
||||
},
|
||||
"appSelectorCard": {
|
||||
"widgetTitle": "Select application",
|
||||
"widgetTitle": "Select an application",
|
||||
"widgetTitleSelected": "Selected application",
|
||||
"widgetSubtitle": "No application selected",
|
||||
"noAppsLabel": "No applications found.",
|
||||
|
||||
"noAppsLabel": "No applications found",
|
||||
"notInstalled":"App not installed",
|
||||
|
||||
"currentVersion": "Current",
|
||||
"recommendedVersion": "Recommended",
|
||||
"anyVersion": "any"
|
||||
"suggestedVersion": "Suggested",
|
||||
"allVersions": "All versions"
|
||||
},
|
||||
"patchSelectorCard": {
|
||||
"widgetTitle": "Select patches",
|
||||
"widgetTitleSelected": "Selected patches",
|
||||
|
||||
"widgetSubtitle": "Select an application first",
|
||||
"widgetEmptySubtitle": "No patches selected"
|
||||
},
|
||||
@@ -66,96 +101,224 @@
|
||||
"widgetSubtitle": "We are online!"
|
||||
},
|
||||
"appSelectorView": {
|
||||
"viewTitle": "Select application",
|
||||
"viewTitle": "Select an application",
|
||||
"searchBarHint": "Search applications",
|
||||
|
||||
"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",
|
||||
|
||||
"featureNotAvailable": "Feature not implemented",
|
||||
"featureNotAvailableText": "This application is a split APK and cannot be selected. Unfortunately, this feature is only available for rooted users at the moment. However, you can still install the application by selecting its APK files from your device's storage instead"
|
||||
},
|
||||
"patchesSelectorView": {
|
||||
"viewTitle": "Select patches",
|
||||
"searchBarHint": "Search patches",
|
||||
"universalPatches": "Universal patches",
|
||||
"newPatches": "New patches",
|
||||
"patches": "Patches",
|
||||
|
||||
"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",
|
||||
"selectAllPatchesWarningTitle": "Warning",
|
||||
"selectAllPatchesWarningContent": "You are about to select all patches, that includes unrecommended patches and can cause unwanted behavior."
|
||||
"setRequiredOption": "Some patches require options to be set:\n\n{patches}\n\nPlease set them before continuing."
|
||||
},
|
||||
"patchOptionsView": {
|
||||
"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": {
|
||||
"unsupportedWarningButton": "Warning",
|
||||
"unsupportedDialogTitle": "Warning",
|
||||
"unsupportedDialogText": "Selecting this patch may result in patching errors.\n\nApp version: {packageVersion}\nCurrent supported versions:\n{supportedVersions}"
|
||||
"unsupportedDialogText": "Selecting this patch may result in patching errors.\n\nApp version: {packageVersion}\nSupported versions:\n{supportedVersions}",
|
||||
"unsupportedPatchVersion": "Patch is not supported for this app version.",
|
||||
"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": {
|
||||
"widgetTitle": "Installer",
|
||||
"installType": "Select install type",
|
||||
"installTypeDescription": "Select the installation type to proceed with.",
|
||||
|
||||
"installButton": "Install",
|
||||
"installRootButton": "Install as Root",
|
||||
"installRootType": "Mount",
|
||||
"installNonRootType": "Normal",
|
||||
|
||||
"pressBackAgain": "Press back again to cancel",
|
||||
"openButton": "Open",
|
||||
"shareButton": "Share file",
|
||||
|
||||
"notificationTitle": "ReVanced Manager is patching",
|
||||
"notificationText": "Tap to return to the installer",
|
||||
"shareApkMenuOption": "Share APK",
|
||||
"shareLogMenuOption": "Share log",
|
||||
"installErrorDialogTitle": "Error",
|
||||
"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.",
|
||||
"installErrorDialogText3": "Root install is not possible as the original APK was selected from storage.\nSelect an installed app or choose non-root install.",
|
||||
"noExit": "Installer is still running..."
|
||||
|
||||
"exportApkButtonTooltip": "Export patched APK",
|
||||
"exportLogButtonTooltip": "Export log",
|
||||
|
||||
"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?",
|
||||
"copiedToClipboard": "Copied log to clipboard",
|
||||
|
||||
"noExit": "Installer is still running, cannot exit..."
|
||||
},
|
||||
"settingsView": {
|
||||
"widgetTitle": "Settings",
|
||||
|
||||
"appearanceSectionTitle": "Appearance",
|
||||
"teamSectionTitle": "Team",
|
||||
"infoSectionTitle": "Info",
|
||||
"debugSectionTitle": "Debugging",
|
||||
"advancedSectionTitle": "Advanced",
|
||||
"darkThemeLabel": "Dark Mode",
|
||||
"darkThemeHint": "Welcome to the Dark Side",
|
||||
"exportSectionTitle": "Import & export",
|
||||
|
||||
"themeModeLabel": "App theme",
|
||||
"systemThemeLabel": "System",
|
||||
"lightThemeLabel": "Light",
|
||||
"darkThemeLabel": "Dark",
|
||||
|
||||
"dynamicThemeLabel": "Material You",
|
||||
"dynamicThemeHint": "Enjoy an experience closer to your device",
|
||||
|
||||
"languageLabel": "Language",
|
||||
"englishOption": "English",
|
||||
"frenchOption": "French",
|
||||
|
||||
"sourcesLabel": "Sources",
|
||||
"sourcesLabelHint": "Configure your custom sources",
|
||||
"orgPatchesLabel": "Patches Organization",
|
||||
"sourcesPatchesLabel": "Patches Source",
|
||||
"orgIntegrationsLabel": "Integrations Organization",
|
||||
"sourcesIntegrationsLabel": "Integrations Source",
|
||||
"sourcesLabelHint": "Configure your sources",
|
||||
"sourcesIntegrationsLabel": "Integrations source",
|
||||
"sourcesResetDialogTitle": "Reset",
|
||||
"sourcesResetDialogText": "Are you sure you want to reset custom sources to their default values?",
|
||||
"apiURLResetDialogText": "Are you sure you want to reset API URL to its default value?",
|
||||
"sourcesResetDialogText": "Are you sure you want to reset your sources to their default values?",
|
||||
"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 your API URL",
|
||||
"selectApiURL": "API URL",
|
||||
"hostRepositoryLabel": "Repository API",
|
||||
"orgPatchesLabel": "Patches organization",
|
||||
"sourcesPatchesLabel": "Patches source",
|
||||
"orgIntegrationsLabel": "Integrations organization",
|
||||
|
||||
"contributorsLabel": "Contributors",
|
||||
"contributorsHint": "A list of contributors of ReVanced",
|
||||
"logsLabel": "Logs",
|
||||
"logsHint": "Share device debug logs",
|
||||
"apiURLLabel": "API URL",
|
||||
"apiURLHint": "Configure your custom API URL",
|
||||
"selectApiURL": "Select URL",
|
||||
|
||||
"logsLabel": "Share logs",
|
||||
"logsHint": "Share ReVanced Manager logs",
|
||||
|
||||
"enablePatchesSelectionLabel": "Allow changing patch selection",
|
||||
"enablePatchesSelectionHint": "Allow changing the selection of 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": "Restricts patches to supported app versions",
|
||||
|
||||
"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": {
|
||||
"widgetTitle": "App Info",
|
||||
"widgetTitle": "App info",
|
||||
"openButton": "Open",
|
||||
"uninstallButton": "Uninstall",
|
||||
"patchButton": "Patch",
|
||||
"unpatchButton": "Unpatch",
|
||||
"unpatchDialogText": "Are you sure you want to unpatch this app?",
|
||||
"rootDialogTitle": "Error",
|
||||
"rootDialogText": "App was installed with root mode enabled but currently root mode is disabled.\nPlease enable root mode first.",
|
||||
"packageNameLabel": "Package Name",
|
||||
"installTypeLabel": "Installation Type",
|
||||
|
||||
"unpatchDialogText": "Are you sure you want to unpatch this app?",
|
||||
"rootDialogText": "App was installed with superuser permissions, but currently ReVanced Manager has no permissions.\nPlease grant superuser permissions first.",
|
||||
|
||||
"packageNameLabel": "Package name",
|
||||
"installTypeLabel": "Installation type",
|
||||
"rootTypeLabel": "Root",
|
||||
"nonRootTypeLabel": "Non-root",
|
||||
"patchedDateLabel": "Patched Date",
|
||||
"patchedDateLabel": "Patched date",
|
||||
"appliedPatchesLabel": "Applied patches",
|
||||
|
||||
"patchedDateHint": "{date} at {time}",
|
||||
"appliedPatchesLabel": "Applied Patches",
|
||||
"appliedPatchesHint": "{quantity} applied patches"
|
||||
"appliedPatchesHint": "{quantity} applied patches",
|
||||
|
||||
"updateNotImplemented": "This feature has not been implemented yet"
|
||||
},
|
||||
"contributorsView": {
|
||||
"widgetTitle": "Contributors",
|
||||
"patcherContributors": "Patcher Contributors",
|
||||
"patchesContributors": "Patches Contributors",
|
||||
"integrationsContributors": "Integrations Contributors",
|
||||
"cliContributors": "CLI Contributors",
|
||||
"managerContributors": "Manager Contributors"
|
||||
"patcherContributors": "Patcher contributors",
|
||||
"patchesContributors": "Patches contributors",
|
||||
"integrationsContributors": "Integrations contributors",
|
||||
"cliContributors": "CLI contributors",
|
||||
"managerContributors": "Manager contributors"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
preserve_hierarchy: 1
|
||||
files:
|
||||
- source: /assets/i18n/en.json
|
||||
- source: /assets/i18n/en_US.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)
|
||||
27
docs/3_troubleshooting.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# ❔ 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 is conflicting with the patched app. Uninstall the existing app before installing the patched app.
|
||||
|
||||
- ❗️ Error code `135`, `139` or `1` when patching the app
|
||||
|
||||
Your device is not supported. Refer to 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)
|
||||
41
docs/4_building.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# 🛠️ 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. Create a GitHub personal access token with the `read:packages` scope [here](https://github.com/settings/tokens/new?scopes=read:packages&description=ReVanced)
|
||||
|
||||
4. Add your GitHub username and the token to `~/android/gradle.properties`
|
||||
|
||||
```properties
|
||||
gpr.user = YourUsername
|
||||
gpr.key = ghp_longrandomkey
|
||||
```
|
||||
|
||||
5. Get dependencies
|
||||
|
||||
```sh
|
||||
flutter pub get
|
||||
```
|
||||
|
||||
6. Delete conflicting outputs
|
||||
|
||||
```sh
|
||||
flutter packages pub run build_runner build --delete-conflicting-outputs
|
||||
```
|
||||
|
||||
> [!Note]
|
||||
> Must be run every time you sync your local repository with the remote repository.
|
||||
|
||||
7. 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
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
5
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
240
gradlew
vendored
Normal file
@@ -0,0 +1,240 @@
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015-2021 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=${0##*/}
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command;
|
||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||
# shell script including quotes and variable substitutions, so put them in
|
||||
# double quotes to make sure that they get re-expanded; and
|
||||
# * put everything else in single quotes, so that it's not re-expanded.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
91
gradlew.bat
vendored
Normal file
@@ -0,0 +1,91 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
@@ -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/manager_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/navigation/navigation_view.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_viewmodel.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: AppSelectorView),
|
||||
MaterialRoute(page: PatchesSelectorView),
|
||||
MaterialRoute(page: PatchOptionsView),
|
||||
MaterialRoute(page: InstallerView),
|
||||
MaterialRoute(page: SettingsView),
|
||||
MaterialRoute(page: ContributorsView),
|
||||
@@ -32,11 +36,13 @@ import 'package:stacked_services/stacked_services.dart';
|
||||
LazySingleton(classType: NavigationViewModel),
|
||||
LazySingleton(classType: HomeViewModel),
|
||||
LazySingleton(classType: PatcherViewModel),
|
||||
LazySingleton(classType: PatchOptionsViewModel),
|
||||
LazySingleton(classType: NavigationService),
|
||||
LazySingleton(classType: ManagerAPI),
|
||||
LazySingleton(classType: PatcherAPI),
|
||||
LazySingleton(classType: RevancedAPI),
|
||||
LazySingleton(classType: GithubAPI),
|
||||
LazySingleton(classType: DownloadManager),
|
||||
LazySingleton(classType: Toast),
|
||||
],
|
||||
)
|
||||
|
||||
@@ -1,26 +1,33 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_i18n/flutter_i18n.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.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/manager_api.dart';
|
||||
import 'package:revanced_manager/services/patcher_api.dart';
|
||||
import 'package:revanced_manager/services/revanced_api.dart';
|
||||
import 'package:revanced_manager/ui/theme/dynamic_theme_builder.dart';
|
||||
import 'package:revanced_manager/ui/views/navigation/navigation_view.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:stacked_themes/stacked_themes.dart';
|
||||
import 'package:timezone/data/latest.dart' as tz;
|
||||
|
||||
late SharedPreferences prefs;
|
||||
Future main() async {
|
||||
await ThemeManager.initialise();
|
||||
await setupLocator();
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await locator<ManagerAPI>().initialize();
|
||||
String apiUrl = locator<ManagerAPI>().getApiUrl();
|
||||
await locator<DownloadManager>().initialize();
|
||||
final String apiUrl = locator<ManagerAPI>().getApiUrl();
|
||||
await locator<RevancedAPI>().initialize(apiUrl);
|
||||
locator<GithubAPI>().initialize();
|
||||
await locator<PatcherAPI>().initialize();
|
||||
final String repoUrl = locator<ManagerAPI>().getRepoUrl();
|
||||
locator<GithubAPI>().initialize(repoUrl);
|
||||
tz.initializeTimeZones();
|
||||
prefs = await SharedPreferences.getInstance();
|
||||
|
||||
runApp(const MyApp());
|
||||
}
|
||||
|
||||
@@ -29,6 +36,12 @@ class MyApp extends StatelessWidget {
|
||||
|
||||
@override
|
||||
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(
|
||||
title: 'ReVanced Manager',
|
||||
home: const NavigationView(),
|
||||
@@ -36,11 +49,18 @@ class MyApp extends StatelessWidget {
|
||||
FlutterI18nDelegate(
|
||||
translationLoader: FileTranslationLoader(
|
||||
fallbackFile: 'en_US',
|
||||
forcedLocale: locale,
|
||||
basePath: 'assets/i18n',
|
||||
useCountryCode: true,
|
||||
),
|
||||
missingTranslationHandler: (key, locale) {
|
||||
log(
|
||||
'--> Missing translation: key: $key, languageCode: ${locale?.languageCode}',
|
||||
);
|
||||
},
|
||||
),
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,45 +1,41 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:revanced_manager/utils/string.dart';
|
||||
|
||||
part 'patch.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class Patch {
|
||||
final String name;
|
||||
final String description;
|
||||
final String version;
|
||||
final bool excluded;
|
||||
final List<String> dependencies;
|
||||
final List<Package> compatiblePackages;
|
||||
|
||||
Patch({
|
||||
required this.name,
|
||||
required this.description,
|
||||
required this.version,
|
||||
required this.excluded,
|
||||
required this.dependencies,
|
||||
required this.compatiblePackages,
|
||||
required this.options,
|
||||
});
|
||||
|
||||
factory Patch.fromJson(Map<String, dynamic> json) => _$PatchFromJson(json);
|
||||
factory Patch.fromJson(Map<String, dynamic> json) {
|
||||
// See: https://github.com/ReVanced/revanced-manager/issues/1364#issuecomment-1760414618
|
||||
if (json['options'] == null) {
|
||||
json['options'] = [];
|
||||
}
|
||||
|
||||
return _$PatchFromJson(json);
|
||||
}
|
||||
|
||||
final String name;
|
||||
final String? description;
|
||||
final bool excluded;
|
||||
final List<Package> compatiblePackages;
|
||||
final List<Option> options;
|
||||
|
||||
Map<String, dynamic> toJson() => _$PatchToJson(this);
|
||||
|
||||
String getSimpleName() {
|
||||
return name
|
||||
.replaceAll('-', ' ')
|
||||
.split('-')
|
||||
.join(' ')
|
||||
.toTitleCase()
|
||||
.replaceFirst('Microg', 'MicroG');
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class Package {
|
||||
final String name;
|
||||
final List<String> versions;
|
||||
|
||||
Package({
|
||||
required this.name,
|
||||
required this.versions,
|
||||
@@ -48,5 +44,31 @@ class Package {
|
||||
factory Package.fromJson(Map<String, dynamic> json) =>
|
||||
_$PackageFromJson(json);
|
||||
|
||||
final String name;
|
||||
final List<String> versions;
|
||||
|
||||
Map toJson() => _$PackageToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class Option {
|
||||
Option({
|
||||
required this.key,
|
||||
required this.title,
|
||||
required this.description,
|
||||
required this.value,
|
||||
required this.required,
|
||||
required this.optionClassType,
|
||||
});
|
||||
|
||||
factory Option.fromJson(Map<String, dynamic> json) => _$OptionFromJson(json);
|
||||
|
||||
final String key;
|
||||
final String title;
|
||||
final String description;
|
||||
dynamic value;
|
||||
final bool required;
|
||||
final String optionClassType;
|
||||
|
||||
Map toJson() => _$OptionToJson(this);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,20 @@ part 'patched_application.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
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 packageName;
|
||||
String version;
|
||||
@@ -18,26 +32,7 @@ class PatchedApplication {
|
||||
DateTime patchDate;
|
||||
bool isRooted;
|
||||
bool isFromStorage;
|
||||
bool hasUpdates;
|
||||
List<String> appliedPatches;
|
||||
List<String> changelog;
|
||||
|
||||
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.hasUpdates = false,
|
||||
this.appliedPatches = const [],
|
||||
this.changelog = const [],
|
||||
});
|
||||
|
||||
factory PatchedApplication.fromJson(Map<String, dynamic> json) =>
|
||||
_$PatchedApplicationFromJson(json);
|
||||
|
||||
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,116 +1,171 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:dio_http_cache_lts/dio_http_cache_lts.dart';
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:revanced_manager/models/patch.dart';
|
||||
import 'package:dio_http2_adapter/dio_http2_adapter.dart';
|
||||
import 'package:revanced_manager/app/app.locator.dart';
|
||||
import 'package:revanced_manager/services/download_manager.dart';
|
||||
import 'package:revanced_manager/services/manager_api.dart';
|
||||
|
||||
@lazySingleton
|
||||
class GithubAPI {
|
||||
final Dio _dio = Dio(
|
||||
BaseOptions(baseUrl: 'https://api.github.com'),
|
||||
)..httpClientAdapter = Http2Adapter(
|
||||
ConnectionManager(
|
||||
idleTimeout: 10000,
|
||||
onClientCreate: (_, config) => config.onBadCertificate = (_) => true,
|
||||
),
|
||||
);
|
||||
final DioCacheManager _dioCacheManager = DioCacheManager(CacheConfig());
|
||||
final Options _cacheOptions = buildCacheOptions(
|
||||
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',
|
||||
};
|
||||
late final Dio _dio;
|
||||
late final ManagerAPI _managerAPI = locator<ManagerAPI>();
|
||||
late final DownloadManager _downloadManager = locator<DownloadManager>();
|
||||
|
||||
void initialize() {
|
||||
_dio.interceptors.add(_dioCacheManager.interceptor);
|
||||
Future<void> initialize(String repoUrl) async {
|
||||
_dio = _downloadManager.initDio(repoUrl);
|
||||
}
|
||||
|
||||
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 {
|
||||
var response = await _dio.get(
|
||||
'/repos/$repoName/releases/latest',
|
||||
options: _cacheOptions,
|
||||
final response = await _dio.get(
|
||||
'/repos/$repoName/releases',
|
||||
);
|
||||
return response.data;
|
||||
} on Exception {
|
||||
return response.data[0];
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<String>> getCommits(
|
||||
String packageName,
|
||||
Future<Map<String, dynamic>?> getPatchesRelease(
|
||||
String repoName,
|
||||
DateTime since,
|
||||
String version,
|
||||
) async {
|
||||
String path =
|
||||
'src/main/kotlin/app/revanced/patches/${repoAppPath[packageName]}';
|
||||
try {
|
||||
var response = await _dio.get(
|
||||
'/repos/$repoName/commits',
|
||||
queryParameters: {
|
||||
'path': path,
|
||||
'per_page': 3,
|
||||
'since': since.toIso8601String(),
|
||||
},
|
||||
options: _cacheOptions,
|
||||
final response = await _dio.get(
|
||||
'/repos/$repoName/releases/tags/$version',
|
||||
);
|
||||
List<dynamic> commits = response.data;
|
||||
return commits
|
||||
.map((commit) =>
|
||||
(commit['commit']['message'] as String).split('\n')[0])
|
||||
.toList();
|
||||
} on Exception {
|
||||
return List.empty();
|
||||
return response.data;
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<File?> getLatestReleaseFile(String extension, String repoName) async {
|
||||
Future<Map<String, dynamic>?> getLatestPatchesRelease(
|
||||
String repoName,
|
||||
) async {
|
||||
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) {
|
||||
Map<String, dynamic>? asset =
|
||||
final Map<String, dynamic>? asset =
|
||||
(release['assets'] as List<dynamic>).firstWhereOrNull(
|
||||
(asset) => (asset['name'] as String).endsWith(extension),
|
||||
);
|
||||
if (asset != null) {
|
||||
return await DefaultCacheManager().getSingleFile(
|
||||
return await _downloadManager.getSingleFile(
|
||||
asset['browser_download_url'],
|
||||
);
|
||||
}
|
||||
}
|
||||
} on Exception {
|
||||
return null;
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<List<Patch>> getPatches(String repoName) async {
|
||||
List<Patch> patches = [];
|
||||
Future<File?> getPatchesReleaseFile(
|
||||
String extension,
|
||||
String repoName,
|
||||
String version,
|
||||
String url,
|
||||
) async {
|
||||
try {
|
||||
File? f = await getLatestReleaseFile('.json', repoName);
|
||||
if (f != null) {
|
||||
List<dynamic> list = jsonDecode(f.readAsStringSync());
|
||||
patches = list.map((patch) => Patch.fromJson(patch)).toList();
|
||||
if (url.isNotEmpty) {
|
||||
return await _downloadManager.getSingleFile(
|
||||
url,
|
||||
);
|
||||
}
|
||||
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,15 +1,24 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
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: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/models/patch.dart';
|
||||
import 'package:revanced_manager/models/patched_application.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/root_api.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
|
||||
import 'package:revanced_manager/utils/check_for_supported_patch.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:timeago/timeago.dart';
|
||||
|
||||
@lazySingleton
|
||||
class ManagerAPI {
|
||||
@@ -19,15 +28,47 @@ class ManagerAPI {
|
||||
final String patcherRepo = 'revanced-patcher';
|
||||
final String cliRepo = 'revanced-cli';
|
||||
late SharedPreferences _prefs;
|
||||
String defaultApiUrl = 'https://revanced-releases-api.afterst0rm.xyz';
|
||||
List<Patch> patches = [];
|
||||
List<Option> modifiedOptions = [];
|
||||
List<Option> options = [];
|
||||
Patch? selectedPatch;
|
||||
BuildContext? ctx;
|
||||
bool isRooted = false;
|
||||
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 defaultPatchesRepo = 'revanced/revanced-patches';
|
||||
String defaultIntegrationsRepo = 'revanced/revanced-integrations';
|
||||
String defaultCliRepo = 'revanced/revanced-cli';
|
||||
String defaultManagerRepo = 'revanced/revanced-manager';
|
||||
String? patchesVersion = '';
|
||||
String? integrationsVersion = '';
|
||||
|
||||
bool isDefaultPatchesRepo() {
|
||||
return getPatchesRepo().toLowerCase() == 'revanced/revanced-patches';
|
||||
}
|
||||
|
||||
bool isDefaultIntegrationsRepo() {
|
||||
return getIntegrationsRepo().toLowerCase() ==
|
||||
'revanced/revanced-integrations';
|
||||
}
|
||||
|
||||
Future<void> initialize() async {
|
||||
_prefs = await SharedPreferences.getInstance();
|
||||
isRooted = await _rootAPI.isRooted();
|
||||
isDynamicThemeAvailable = (await getSdkVersion()) >= 31; // ANDROID_12_SDK_VERSION = 31
|
||||
storedPatchesFile =
|
||||
(await getApplicationDocumentsDirectory()).path + storedPatchesFile;
|
||||
}
|
||||
|
||||
Future<int> getSdkVersion() async {
|
||||
final AndroidDeviceInfo info = await DeviceInfoPlugin().androidInfo;
|
||||
return info.version.sdkInt;
|
||||
}
|
||||
|
||||
String getApiUrl() {
|
||||
@@ -43,6 +84,25 @@ class ManagerAPI {
|
||||
await _prefs.setString('apiUrl', url);
|
||||
}
|
||||
|
||||
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() {
|
||||
return _prefs.getString('patchesRepo') ?? defaultPatchesRepo;
|
||||
}
|
||||
@@ -54,6 +114,109 @@ class ManagerAPI {
|
||||
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() {
|
||||
return _prefs.getString('integrationsRepo') ?? defaultIntegrationsRepo;
|
||||
}
|
||||
@@ -73,31 +236,75 @@ class ManagerAPI {
|
||||
await _prefs.setBool('useDynamicTheme', value);
|
||||
}
|
||||
|
||||
bool getUseDarkTheme() {
|
||||
return _prefs.getBool('useDarkTheme') ?? false;
|
||||
int getThemeMode() {
|
||||
return _prefs.getInt('themeMode') ?? 2;
|
||||
}
|
||||
|
||||
Future<void> setUseDarkTheme(bool value) async {
|
||||
await _prefs.setBool('useDarkTheme', value);
|
||||
Future<void> setThemeMode(int value) async {
|
||||
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);
|
||||
}
|
||||
|
||||
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<String> apps = _prefs.getStringList('patchedApps') ?? [];
|
||||
final List<String> apps = _prefs.getStringList('patchedApps') ?? [];
|
||||
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) {
|
||||
patchedApps.sort((a, b) => a.name.compareTo(b.name));
|
||||
}
|
||||
await _prefs.setStringList('patchedApps',
|
||||
patchedApps.map((a) => json.encode(a.toJson())).toList());
|
||||
await _prefs.setStringList(
|
||||
'patchedApps',
|
||||
patchedApps.map((a) => json.encode(a.toJson())).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> savePatchedApp(PatchedApplication app) async {
|
||||
List<PatchedApplication> patchedApps = getPatchedApps();
|
||||
final List<PatchedApplication> patchedApps = getPatchedApps();
|
||||
patchedApps.removeWhere((a) => a.packageName == app.packageName);
|
||||
ApplicationWithIcon? installed = await DeviceApps.getApp(
|
||||
final ApplicationWithIcon? installed = await DeviceApps.getApp(
|
||||
app.packageName,
|
||||
true,
|
||||
) as ApplicationWithIcon?;
|
||||
@@ -111,14 +318,20 @@ class ManagerAPI {
|
||||
}
|
||||
|
||||
Future<void> deletePatchedApp(PatchedApplication app) async {
|
||||
List<PatchedApplication> patchedApps = getPatchedApps();
|
||||
final List<PatchedApplication> patchedApps = getPatchedApps();
|
||||
patchedApps.removeWhere((a) => a.packageName == app.packageName);
|
||||
await setPatchedApps(patchedApps);
|
||||
}
|
||||
|
||||
void clearAllData() {
|
||||
_revancedAPI.clearAllCache();
|
||||
_githubAPI.clearAllCache();
|
||||
Future<void> clearAllData() async {
|
||||
try {
|
||||
_revancedAPI.clearAllCache();
|
||||
_githubAPI.clearAllCache();
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, List<dynamic>>> getContributors() async {
|
||||
@@ -126,48 +339,109 @@ class ManagerAPI {
|
||||
}
|
||||
|
||||
Future<List<Patch>> getPatches() async {
|
||||
String repoName = getPatchesRepo();
|
||||
if (repoName == defaultPatchesRepo) {
|
||||
return await _revancedAPI.getPatches();
|
||||
} else {
|
||||
return await _githubAPI.getPatches(repoName);
|
||||
if (patches.isNotEmpty) {
|
||||
return patches;
|
||||
}
|
||||
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 {
|
||||
String repoName = getPatchesRepo();
|
||||
if (repoName == defaultPatchesRepo) {
|
||||
return await _revancedAPI.getLatestReleaseFile(
|
||||
try {
|
||||
final String repoName = getPatchesRepo();
|
||||
final String currentVersion = await getCurrentPatchesVersion();
|
||||
final String url = getPatchesDownloadURL();
|
||||
return await _githubAPI.getPatchesReleaseFile(
|
||||
'.jar',
|
||||
defaultPatchesRepo,
|
||||
repoName,
|
||||
currentVersion,
|
||||
url,
|
||||
);
|
||||
} else {
|
||||
return await _githubAPI.getLatestReleaseFile('.jar', repoName);
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<File?> downloadIntegrations() async {
|
||||
String repoName = getIntegrationsRepo();
|
||||
if (repoName == defaultIntegrationsRepo) {
|
||||
return await _revancedAPI.getLatestReleaseFile(
|
||||
try {
|
||||
final String repoName = getIntegrationsRepo();
|
||||
final String currentVersion = await getCurrentIntegrationsVersion();
|
||||
final String url = getIntegrationsDownloadURL();
|
||||
return await _githubAPI.getPatchesReleaseFile(
|
||||
'.apk',
|
||||
defaultIntegrationsRepo,
|
||||
repoName,
|
||||
currentVersion,
|
||||
url,
|
||||
);
|
||||
} else {
|
||||
return await _githubAPI.getLatestReleaseFile('.apk', repoName);
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<File?> downloadManager() async {
|
||||
return await _revancedAPI.getLatestReleaseFile('.apk', defaultManagerRepo);
|
||||
return await _revancedAPI.getLatestReleaseFile(
|
||||
'.apk',
|
||||
defaultManagerRepo,
|
||||
);
|
||||
}
|
||||
|
||||
Future<String?> getLatestPatcherReleaseTime() async {
|
||||
return await _revancedAPI.getLatestReleaseTime('.gz', defaultPatcherRepo);
|
||||
Future<String?> getLatestPatchesReleaseTime() async {
|
||||
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 {
|
||||
return await _revancedAPI.getLatestReleaseTime('.apk', defaultManagerRepo);
|
||||
return await _revancedAPI.getLatestReleaseTime(
|
||||
'.apk',
|
||||
defaultManagerRepo,
|
||||
);
|
||||
}
|
||||
|
||||
Future<String?> getLatestManagerVersion() async {
|
||||
@@ -177,17 +451,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 {
|
||||
PackageInfo packageInfo = await PackageInfo.fromPlatform();
|
||||
final PackageInfo packageInfo = await PackageInfo.fromPlatform();
|
||||
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(
|
||||
List<PatchedApplication> patchedApps,
|
||||
) async {
|
||||
List<PatchedApplication> toRemove = [];
|
||||
for (PatchedApplication app in patchedApps) {
|
||||
bool isRemove = await isAppUninstalled(app);
|
||||
final List<PatchedApplication> toRemove = [];
|
||||
for (final PatchedApplication app in patchedApps) {
|
||||
final bool isRemove = await isAppUninstalled(app);
|
||||
if (isRemove) {
|
||||
toRemove.add(app);
|
||||
}
|
||||
@@ -195,49 +539,18 @@ class ManagerAPI {
|
||||
return toRemove;
|
||||
}
|
||||
|
||||
Future<List<PatchedApplication>> getUnsavedApps(
|
||||
List<PatchedApplication> patchedApps,
|
||||
) async {
|
||||
List<PatchedApplication> unsavedApps = [];
|
||||
bool hasRootPermissions = await _rootAPI.hasRootPermissions();
|
||||
Future<List<PatchedApplication>> getMountedApps() async {
|
||||
final List<PatchedApplication> mountedApps = [];
|
||||
final bool hasRootPermissions = await _rootAPI.hasRootPermissions();
|
||||
if (hasRootPermissions) {
|
||||
List<String> installedApps = await _rootAPI.getInstalledApps();
|
||||
for (String packageName in installedApps) {
|
||||
if (!patchedApps.any((app) => app.packageName == packageName)) {
|
||||
ApplicationWithIcon? application = await DeviceApps.getApp(
|
||||
packageName,
|
||||
true,
|
||||
) as ApplicationWithIcon?;
|
||||
if (application != null) {
|
||||
unsavedApps.add(
|
||||
PatchedApplication(
|
||||
name: application.appName,
|
||||
packageName: 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,
|
||||
final List<String> installedApps = await _rootAPI.getInstalledApps();
|
||||
for (final String packageName in installedApps) {
|
||||
final ApplicationWithIcon? application = await DeviceApps.getApp(
|
||||
packageName,
|
||||
true,
|
||||
) as ApplicationWithIcon?;
|
||||
if (application != null) {
|
||||
unsavedApps.add(
|
||||
mountedApps.add(
|
||||
PatchedApplication(
|
||||
name: application.appName,
|
||||
packageName: application.packageName,
|
||||
@@ -245,47 +558,97 @@ class ManagerAPI {
|
||||
apkFilePath: application.apkFilePath,
|
||||
icon: application.icon,
|
||||
patchDate: DateTime.now(),
|
||||
isRooted: false,
|
||||
isRooted: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return unsavedApps;
|
||||
|
||||
return mountedApps;
|
||||
}
|
||||
|
||||
Future<void> showPatchesChangeWarningDialog(BuildContext context) {
|
||||
final ValueNotifier<bool> noShow =
|
||||
ValueNotifier(!showPatchesChangeWarning());
|
||||
return showDialog(
|
||||
barrierDismissible: false,
|
||||
context: context,
|
||||
builder: (context) => WillPopScope(
|
||||
onWillPop: () async => false,
|
||||
child: AlertDialog(
|
||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
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),
|
||||
CheckboxListTile(
|
||||
value: value,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: I18nText(
|
||||
'noShowAgain',
|
||||
),
|
||||
onChanged: (selected) {
|
||||
noShow.value = selected!;
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
CustomMaterialButton(
|
||||
label: I18nText('okButton'),
|
||||
onPressed: () {
|
||||
setPatchesChangeWarning(noShow.value);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> reAssessSavedApps() async {
|
||||
List<PatchedApplication> patchedApps = getPatchedApps();
|
||||
List<PatchedApplication> unsavedApps = await getUnsavedApps(patchedApps);
|
||||
patchedApps.addAll(unsavedApps);
|
||||
List<PatchedApplication> toRemove = await getAppsToRemove(patchedApps);
|
||||
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));
|
||||
for (PatchedApplication app in patchedApps) {
|
||||
app.hasUpdates = await hasAppUpdates(app.packageName, app.patchDate);
|
||||
app.changelog = await getAppChangelog(app.packageName, app.patchDate);
|
||||
if (!app.hasUpdates) {
|
||||
String? currentInstalledVersion =
|
||||
(await DeviceApps.getApp(app.packageName))?.versionName;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Determine all apps that are installed by mounting.
|
||||
final List<PatchedApplication> mountedApps = await getMountedApps();
|
||||
mountedApps.removeWhere(
|
||||
(app) => patchedApps
|
||||
.any((patchedApp) => patchedApp.packageName == app.packageName),
|
||||
);
|
||||
patchedApps.addAll(mountedApps);
|
||||
|
||||
await setPatchedApps(patchedApps);
|
||||
}
|
||||
|
||||
Future<bool> isAppUninstalled(PatchedApplication app) async {
|
||||
bool existsRoot = false;
|
||||
bool existsNonRoot = await DeviceApps.isAppInstalled(app.packageName);
|
||||
final bool existsNonRoot = await DeviceApps.isAppInstalled(app.packageName);
|
||||
if (app.isRooted) {
|
||||
bool hasRootPermissions = await _rootAPI.hasRootPermissions();
|
||||
final bool hasRootPermissions = await _rootAPI.hasRootPermissions();
|
||||
if (hasRootPermissions) {
|
||||
existsRoot = await _rootAPI.isAppInstalled(app.packageName);
|
||||
}
|
||||
@@ -294,32 +657,6 @@ class ManagerAPI {
|
||||
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(),
|
||||
DateTime(2022, 3, 20, 21, 06, 01),
|
||||
);
|
||||
}
|
||||
return newCommits;
|
||||
}
|
||||
|
||||
Future<bool> isSplitApk(PatchedApplication patchedApp) async {
|
||||
Application? app;
|
||||
if (patchedApp.isFromStorage) {
|
||||
@@ -329,4 +666,74 @@ class ManagerAPI {
|
||||
}
|
||||
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,9 +1,12 @@
|
||||
import 'dart:io';
|
||||
import 'package:app_installer/app_installer.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:cr_file_saver/file_saver.dart';
|
||||
import 'package:device_apps/device_apps.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:install_plugin/install_plugin.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:revanced_manager/app/app.locator.dart';
|
||||
import 'package:revanced_manager/models/patch.dart';
|
||||
@@ -15,19 +18,25 @@ import 'package:share_extend/share_extend.dart';
|
||||
@lazySingleton
|
||||
class PatcherAPI {
|
||||
static const patcherChannel =
|
||||
MethodChannel('app.revanced.manager.flutter/patcher');
|
||||
MethodChannel('app.revanced.manager.flutter/patcher');
|
||||
final ManagerAPI _managerAPI = locator<ManagerAPI>();
|
||||
final RootAPI _rootAPI = RootAPI();
|
||||
late Directory _dataDir;
|
||||
late Directory _tmpDir;
|
||||
late File _keyStoreFile;
|
||||
List<Patch> _patches = [];
|
||||
File? _outFile;
|
||||
List<Patch> _universalPatches = [];
|
||||
List<String> _compatiblePackages = [];
|
||||
Map filteredPatches = <String, List<Patch>>{};
|
||||
File? outFile;
|
||||
|
||||
Future<void> initialize() async {
|
||||
await _loadPatches();
|
||||
Directory appCache = await getTemporaryDirectory();
|
||||
await loadPatches();
|
||||
await _managerAPI.downloadIntegrations();
|
||||
final Directory appCache = await getTemporaryDirectory();
|
||||
_dataDir = await getExternalStorageDirectory() ?? appCache;
|
||||
_tmpDir = Directory('${appCache.path}/patcher');
|
||||
_keyStoreFile = File('${appCache.path}/revanced-manager.keystore');
|
||||
_keyStoreFile = File('${_dataDir.path}/revanced-manager.keystore');
|
||||
cleanPatcher();
|
||||
}
|
||||
|
||||
@@ -37,234 +46,279 @@ 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 {
|
||||
if (_patches.isEmpty) {
|
||||
_patches = await _managerAPI.getPatches();
|
||||
}
|
||||
} on Exception {
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
_patches = List.empty();
|
||||
}
|
||||
|
||||
_compatiblePackages = getCompatiblePackages();
|
||||
_universalPatches = getUniversalPatches();
|
||||
}
|
||||
|
||||
Future<List<ApplicationWithIcon>> getFilteredInstalledApps() async {
|
||||
List<ApplicationWithIcon> filteredApps = [];
|
||||
for (Patch patch in _patches) {
|
||||
for (Package package in patch.compatiblePackages) {
|
||||
try {
|
||||
if (!filteredApps.any((app) => app.packageName == package.name)) {
|
||||
ApplicationWithIcon? app = await DeviceApps.getApp(
|
||||
package.name,
|
||||
true,
|
||||
) as ApplicationWithIcon?;
|
||||
if (app != null) {
|
||||
filteredApps.add(app);
|
||||
}
|
||||
Future<List<ApplicationWithIcon>> getFilteredInstalledApps(
|
||||
bool showUniversalPatches,) async {
|
||||
final List<ApplicationWithIcon> filteredApps = [];
|
||||
final bool allAppsIncluded =
|
||||
_universalPatches.isNotEmpty && showUniversalPatches;
|
||||
if (allAppsIncluded) {
|
||||
final appList = await DeviceApps.getInstalledApplications(
|
||||
includeAppIcons: true,
|
||||
onlyAppsWithLaunchIntent: true,
|
||||
);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
Future<List<Patch>> getFilteredPatches(String packageName) async {
|
||||
String newPackageName = packageName.replaceFirst(
|
||||
'app.revanced.',
|
||||
'com.google.',
|
||||
);
|
||||
return _patches
|
||||
.where((patch) =>
|
||||
!patch.name.contains('settings') &&
|
||||
patch.compatiblePackages.any((pack) => pack.name == newPackageName))
|
||||
List<Patch> getFilteredPatches(String packageName) {
|
||||
if (!_compatiblePackages.contains(packageName)) {
|
||||
return _universalPatches;
|
||||
}
|
||||
|
||||
final List<Patch> patches = _patches
|
||||
.where(
|
||||
(patch) =>
|
||||
patch.compatiblePackages.isEmpty ||
|
||||
!patch.name.contains('settings') &&
|
||||
patch.compatiblePackages
|
||||
.any((pack) => pack.name == packageName),
|
||||
)
|
||||
.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
|
||||
.where((patch) => appliedPatches.contains(patch.name))
|
||||
.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(
|
||||
String packageName,
|
||||
String originalFilePath,
|
||||
List<Patch> selectedPatches,
|
||||
) async {
|
||||
bool mergeIntegrations = await needsIntegrations(selectedPatches);
|
||||
bool includeSettings = await needsSettingsPatch(selectedPatches);
|
||||
if (includeSettings) {
|
||||
try {
|
||||
Patch? settingsPatch = _patches.firstWhereOrNull(
|
||||
(patch) =>
|
||||
patch.name.contains('settings') &&
|
||||
patch.compatiblePackages.any((pack) => pack.name == packageName),
|
||||
);
|
||||
if (settingsPatch != null) {
|
||||
selectedPatches.add(settingsPatch);
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
File? patchBundleFile = await _managerAPI.downloadPatches();
|
||||
File? integrationsFile;
|
||||
if (mergeIntegrations) {
|
||||
integrationsFile = await _managerAPI.downloadIntegrations();
|
||||
}
|
||||
if (patchBundleFile != null) {
|
||||
_tmpDir.createSync();
|
||||
Directory workDir = _tmpDir.createTempSync('tmp-');
|
||||
File inputFile = File('${workDir.path}/base.apk');
|
||||
File patchedFile = File('${workDir.path}/patched.apk');
|
||||
_outFile = File('${workDir.path}/out.apk');
|
||||
Directory cacheDir = Directory('${workDir.path}/cache');
|
||||
cacheDir.createSync();
|
||||
await patcherChannel.invokeMethod(
|
||||
'runPatcher',
|
||||
{
|
||||
'patchBundleFilePath': patchBundleFile.path,
|
||||
'originalFilePath': await getOriginalFilePath(
|
||||
packageName,
|
||||
originalFilePath,
|
||||
),
|
||||
'inputFilePath': inputFile.path,
|
||||
'patchedFilePath': patchedFile.path,
|
||||
'outFilePath': _outFile!.path,
|
||||
'integrationsPath': mergeIntegrations ? integrationsFile!.path : '',
|
||||
'selectedPatches': selectedPatches.map((p) => p.name).toList(),
|
||||
'cacheDirPath': cacheDir.path,
|
||||
'mergeIntegrations': mergeIntegrations,
|
||||
'keyStoreFilePath': _keyStoreFile.path,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> installPatchedFile(PatchedApplication patchedApp) async {
|
||||
if (_outFile != null) {
|
||||
try {
|
||||
if (patchedApp.isRooted) {
|
||||
bool hasRootPermissions = await _rootAPI.hasRootPermissions();
|
||||
if (hasRootPermissions) {
|
||||
return _rootAPI.installApp(
|
||||
patchedApp.packageName,
|
||||
patchedApp.apkFilePath,
|
||||
_outFile!.path,
|
||||
);
|
||||
Future<void> runPatcher(String packageName,
|
||||
String apkFilePath,
|
||||
List<Patch> selectedPatches,) async {
|
||||
final File? integrationsFile = await _managerAPI.downloadIntegrations();
|
||||
final Map<String, Map<String, dynamic>> options = {};
|
||||
for (final patch in selectedPatches) {
|
||||
if (patch.options.isNotEmpty) {
|
||||
final Map<String, dynamic> patchOptions = {};
|
||||
for (final option in patch.options) {
|
||||
final patchOption = _managerAPI.getPatchOption(packageName, patch.name, option.key);
|
||||
if (patchOption != null) {
|
||||
patchOptions[patchOption.key] = patchOption.value;
|
||||
}
|
||||
} else {
|
||||
await AppInstaller.installApk(_outFile!.path);
|
||||
return await DeviceApps.isAppInstalled(patchedApp.packageName);
|
||||
}
|
||||
} on Exception {
|
||||
return false;
|
||||
options[patch.name] = patchOptions;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void sharePatchedFile(String appName, String version) {
|
||||
if (_outFile != null) {
|
||||
String prefix = appName.toLowerCase().replaceAll(' ', '-');
|
||||
String newName = '$prefix-revanced_v$version.apk';
|
||||
int lastSeparator = _outFile!.path.lastIndexOf('/');
|
||||
String newPath = _outFile!.path.substring(0, lastSeparator + 1) + newName;
|
||||
File shareFile = _outFile!.copySync(newPath);
|
||||
ShareExtend.share(shareFile.path, 'file');
|
||||
}
|
||||
}
|
||||
if (integrationsFile != null) {
|
||||
_dataDir.createSync();
|
||||
_tmpDir.createSync();
|
||||
final Directory workDir = _tmpDir.createTempSync('tmp-');
|
||||
final File inputFile = File('${workDir.path}/base.apk');
|
||||
final File patchedFile = File('${workDir.path}/patched.apk');
|
||||
outFile = File('${workDir.path}/out.apk');
|
||||
final Directory cacheDir = Directory('${workDir.path}/cache');
|
||||
cacheDir.createSync();
|
||||
final String originalFilePath = apkFilePath;
|
||||
|
||||
Future<void> sharePatcherLog(String logs) async {
|
||||
Directory appCache = await getTemporaryDirectory();
|
||||
Directory logDir = Directory('${appCache.path}/logs');
|
||||
logDir.createSync();
|
||||
String dateTime = DateTime.now()
|
||||
.toIso8601String()
|
||||
.replaceAll('-', '')
|
||||
.replaceAll(':', '')
|
||||
.replaceAll('T', '')
|
||||
.replaceAll('.', '');
|
||||
File log = File('${logDir.path}/revanced-manager_patcher_$dateTime.log');
|
||||
log.writeAsStringSync(logs);
|
||||
ShareExtend.share(log.path, 'file');
|
||||
}
|
||||
|
||||
String getRecommendedVersion(String packageName) {
|
||||
Map<String, int> versions = {};
|
||||
for (Patch patch in _patches) {
|
||||
Package? package = patch.compatiblePackages.firstWhereOrNull(
|
||||
(pack) => pack.name == packageName,
|
||||
);
|
||||
if (package != null) {
|
||||
for (String version in package.versions) {
|
||||
versions.update(
|
||||
version,
|
||||
(value) => versions[version]! + 1,
|
||||
ifAbsent: () => 1,
|
||||
);
|
||||
try {
|
||||
await patcherChannel.invokeMethod(
|
||||
'runPatcher',
|
||||
{
|
||||
'originalFilePath': originalFilePath,
|
||||
'inputFilePath': inputFile.path,
|
||||
'patchedFilePath': patchedFile.path,
|
||||
'outFilePath': outFile!.path,
|
||||
'integrationsPath': integrationsFile.path,
|
||||
'selectedPatches': selectedPatches.map((p) => p.name).toList(),
|
||||
'options': options,
|
||||
'cacheDirPath': cacheDir.path,
|
||||
'keyStoreFilePath': _keyStoreFile.path,
|
||||
'keystorePassword': _managerAPI.getKeystorePassword(),
|
||||
},
|
||||
);
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (versions.isNotEmpty) {
|
||||
var entries = versions.entries.toList()
|
||||
..sort((a, b) => a.value.compareTo(b.value));
|
||||
versions
|
||||
..clear()
|
||||
..addEntries(entries);
|
||||
versions.removeWhere((key, value) => value != versions.values.last);
|
||||
return (versions.keys.toList()..sort()).last;
|
||||
}
|
||||
|
||||
Future<void> stopPatcher() async {
|
||||
try {
|
||||
await patcherChannel.invokeMethod('stopPatcher');
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> installPatchedFile(PatchedApplication patchedApp) async {
|
||||
if (outFile != null) {
|
||||
try {
|
||||
if (patchedApp.isRooted) {
|
||||
final bool hasRootPermissions = await _rootAPI.hasRootPermissions();
|
||||
if (hasRootPermissions) {
|
||||
return _rootAPI.installApp(
|
||||
patchedApp.packageName,
|
||||
patchedApp.apkFilePath,
|
||||
outFile!.path,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
final install = await InstallPlugin.installApk(outFile!.path);
|
||||
return install['isSuccess'];
|
||||
}
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void exportPatchedFile(String appName, String version) {
|
||||
try {
|
||||
if (outFile != null) {
|
||||
final String newName = _getFileName(appName, version);
|
||||
CRFileSaver.saveFileWithDialog(
|
||||
SaveFileDialogParams(
|
||||
sourceFilePath: outFile!.path,
|
||||
destinationFileName: newName,
|
||||
),
|
||||
);
|
||||
}
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void sharePatchedFile(String appName, String version) {
|
||||
try {
|
||||
if (outFile != null) {
|
||||
final String newName = _getFileName(appName, version);
|
||||
final int lastSeparator = outFile!.path.lastIndexOf('/');
|
||||
final String newPath =
|
||||
outFile!.path.substring(0, lastSeparator + 1) + newName;
|
||||
final File shareFile = outFile!.copySync(newPath);
|
||||
ShareExtend.share(shareFile.path, 'file');
|
||||
}
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String _getFileName(String appName, String version) {
|
||||
final String prefix = appName.toLowerCase().replaceAll(' ', '-');
|
||||
final String newName = '$prefix-revanced_v$version.apk';
|
||||
return newName;
|
||||
}
|
||||
|
||||
Future<void> exportPatcherLog(String logs) async {
|
||||
final Directory appCache = await getTemporaryDirectory();
|
||||
final Directory logDir = Directory('${appCache.path}/logs');
|
||||
logDir.createSync();
|
||||
final String dateTime = DateTime.now()
|
||||
.toIso8601String()
|
||||
.replaceAll('-', '')
|
||||
.replaceAll(':', '')
|
||||
.replaceAll('T', '')
|
||||
.replaceAll('.', '');
|
||||
final String fileName = 'revanced-manager_patcher_$dateTime.txt';
|
||||
final File log = File('${logDir.path}/$fileName');
|
||||
log.writeAsStringSync(logs);
|
||||
CRFileSaver.saveFileWithDialog(
|
||||
SaveFileDialogParams(
|
||||
sourceFilePath: log.path,
|
||||
destinationFileName: fileName,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String getSuggestedVersion(String packageName) {
|
||||
final Map<String, int> versions = {};
|
||||
for (final Patch patch in _patches) {
|
||||
final Package? package = patch.compatiblePackages.firstWhereOrNull(
|
||||
(pack) => pack.name == packageName,
|
||||
);
|
||||
if (package != null) {
|
||||
for (final String version in package.versions) {
|
||||
versions.update(
|
||||
version,
|
||||
(value) => versions[version]! + 1,
|
||||
ifAbsent: () => 1,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (versions.isNotEmpty) {
|
||||
final entries = versions.entries.toList()
|
||||
..sort((a, b) => a.value.compareTo(b.value));
|
||||
versions
|
||||
..clear()
|
||||
..addEntries(entries);
|
||||
versions.removeWhere((key, value) => value != versions.values.last);
|
||||
return (versions.keys.toList()
|
||||
..sort()).last;
|
||||
}
|
||||
return '';
|
||||
}}
|
||||
|
||||
@@ -1,79 +1,69 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:collection/collection.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:revanced_manager/models/patch.dart';
|
||||
import 'package:revanced_manager/app/app.locator.dart';
|
||||
import 'package:revanced_manager/services/download_manager.dart';
|
||||
import 'package:synchronized/synchronized.dart';
|
||||
import 'package:timeago/timeago.dart';
|
||||
import 'package:dio_http2_adapter/dio_http2_adapter.dart';
|
||||
|
||||
@lazySingleton
|
||||
class RevancedAPI {
|
||||
late Dio _dio = Dio();
|
||||
final DioCacheManager _dioCacheManager = DioCacheManager(CacheConfig());
|
||||
final Options _cacheOptions = buildCacheOptions(
|
||||
const Duration(hours: 6),
|
||||
maxStale: const Duration(days: 1),
|
||||
);
|
||||
late final Dio _dio;
|
||||
late final DownloadManager _downloadManager = locator<DownloadManager>();
|
||||
|
||||
Future<void> initialize(String apiUrl) async {
|
||||
_dio = Dio(BaseOptions(
|
||||
baseUrl: apiUrl,
|
||||
))
|
||||
..httpClientAdapter = Http2Adapter(
|
||||
ConnectionManager(
|
||||
idleTimeout: 10000,
|
||||
onClientCreate: (_, config) => config.onBadCertificate = (_) => true,
|
||||
),
|
||||
);
|
||||
_dio.interceptors.add(_dioCacheManager.interceptor);
|
||||
final Lock getToolsLock = Lock();
|
||||
|
||||
Future<void> initialize(String repoUrl) async {
|
||||
_dio = _downloadManager.initDio(repoUrl);
|
||||
}
|
||||
|
||||
Future<void> clearAllCache() async {
|
||||
await _dioCacheManager.clearAll();
|
||||
await _downloadManager.clearAllCache();
|
||||
}
|
||||
|
||||
Future<Map<String, List<dynamic>>> getContributors() async {
|
||||
Map<String, List<dynamic>> contributors = {};
|
||||
final Map<String, List<dynamic>> contributors = {};
|
||||
try {
|
||||
var response = await _dio.get('/contributors', options: _cacheOptions);
|
||||
List<dynamic> repositories = response.data['repositories'];
|
||||
for (Map<String, dynamic> repo in repositories) {
|
||||
String name = repo['name'];
|
||||
final response = await _dio.get('/contributors');
|
||||
final List<dynamic> repositories = response.data['repositories'];
|
||||
for (final Map<String, dynamic> repo in repositories) {
|
||||
final String name = repo['name'];
|
||||
contributors[name] = repo['contributors'];
|
||||
}
|
||||
} on Exception {
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
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(
|
||||
String extension,
|
||||
String repoName,
|
||||
) async {
|
||||
try {
|
||||
var response = await _dio.get('/tools', options: _cacheOptions);
|
||||
List<dynamic> tools = response.data['tools'];
|
||||
return tools.firstWhereOrNull(
|
||||
(t) =>
|
||||
t['repository'] == repoName &&
|
||||
(t['name'] as String).endsWith(extension),
|
||||
);
|
||||
} on Exception {
|
||||
return null;
|
||||
}
|
||||
) {
|
||||
return getToolsLock.synchronized(() async {
|
||||
try {
|
||||
final response = await _dio.get('/tools');
|
||||
final List<dynamic> tools = response.data['tools'];
|
||||
return tools.firstWhereOrNull(
|
||||
(t) =>
|
||||
t['repository'] == repoName &&
|
||||
(t['name'] as String).endsWith(extension),
|
||||
);
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<String?> getLatestReleaseVersion(
|
||||
@@ -81,49 +71,100 @@ class RevancedAPI {
|
||||
String repoName,
|
||||
) async {
|
||||
try {
|
||||
Map<String, dynamic>? release = await _getLatestRelease(
|
||||
final Map<String, dynamic>? release = await _getLatestRelease(
|
||||
extension,
|
||||
repoName,
|
||||
);
|
||||
if (release != null) {
|
||||
return release['version'];
|
||||
}
|
||||
} on Exception {
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<File?> getLatestReleaseFile(String extension, String repoName) async {
|
||||
Future<File?> getLatestReleaseFile(
|
||||
String extension,
|
||||
String repoName,
|
||||
) async {
|
||||
try {
|
||||
Map<String, dynamic>? release = await _getLatestRelease(
|
||||
final Map<String, dynamic>? release = await _getLatestRelease(
|
||||
extension,
|
||||
repoName,
|
||||
);
|
||||
if (release != null) {
|
||||
String url = release['browser_download_url'];
|
||||
return await DefaultCacheManager().getSingleFile(url);
|
||||
final String url = release['browser_download_url'];
|
||||
return await _downloadManager.getSingleFile(url);
|
||||
}
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
} on Exception {
|
||||
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(
|
||||
String extension,
|
||||
String repoName,
|
||||
) async {
|
||||
try {
|
||||
Map<String, dynamic>? release = await _getLatestRelease(
|
||||
final Map<String, dynamic>? release = await _getLatestRelease(
|
||||
extension,
|
||||
repoName,
|
||||
);
|
||||
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');
|
||||
}
|
||||
} on Exception {
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:root/root.dart';
|
||||
|
||||
class RootAPI {
|
||||
final String _managerDirPath = '/data/local/tmp/revanced-manager';
|
||||
// TODO(ponces): remove in the future, keep it for now during migration.
|
||||
final String _revancedOldDirPath = '/data/local/tmp/revanced-manager';
|
||||
final String _revancedDirPath = '/data/adb/revanced';
|
||||
final String _postFsDataDirPath = '/data/adb/post-fs-data.d';
|
||||
final String _serviceDDirPath = '/data/adb/service.d';
|
||||
|
||||
Future<bool> isRooted() async {
|
||||
try {
|
||||
bool? isRooted = await Root.isRootAvailable();
|
||||
final bool? isRooted = await Root.isRootAvailable();
|
||||
return isRooted != null && isRooted;
|
||||
} on Exception {
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -22,7 +28,10 @@ class RootAPI {
|
||||
return isRooted != null && isRooted;
|
||||
}
|
||||
return false;
|
||||
} on Exception {
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -33,52 +42,62 @@ class RootAPI {
|
||||
seLinux,
|
||||
String filePath,
|
||||
) async {
|
||||
if (permissions.isNotEmpty) {
|
||||
await Root.exec(
|
||||
cmd: 'chmod $permissions "$filePath"',
|
||||
);
|
||||
}
|
||||
if (ownerGroup.isNotEmpty) {
|
||||
await Root.exec(
|
||||
cmd: 'chown $ownerGroup "$filePath"',
|
||||
);
|
||||
}
|
||||
if (seLinux.isNotEmpty) {
|
||||
await Root.exec(
|
||||
cmd: 'chcon $seLinux "$filePath"',
|
||||
);
|
||||
try {
|
||||
if (permissions.isNotEmpty) {
|
||||
await Root.exec(
|
||||
cmd: 'chmod $permissions "$filePath"',
|
||||
);
|
||||
}
|
||||
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 {
|
||||
if (packageName.isNotEmpty) {
|
||||
String? res = await Root.exec(
|
||||
cmd: 'ls "$_managerDirPath/$packageName"',
|
||||
);
|
||||
if (res != null && res.isNotEmpty) {
|
||||
res = await Root.exec(
|
||||
cmd: 'ls "$_serviceDDirPath/$packageName.sh"',
|
||||
);
|
||||
return res != null && res.isNotEmpty;
|
||||
}
|
||||
return fileExists('$_serviceDDirPath/$packageName.sh');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<List<String>> getInstalledApps() async {
|
||||
final List<String> apps = List.empty(growable: true);
|
||||
try {
|
||||
String? res = await Root.exec(
|
||||
cmd: 'ls "$_managerDirPath"',
|
||||
cmd: 'ls "$_revancedDirPath"',
|
||||
);
|
||||
if (res != null) {
|
||||
List<String> apps = res.split('\n');
|
||||
apps.removeWhere((pack) => pack.isEmpty);
|
||||
return apps.map((pack) => pack.trim()).toList();
|
||||
final List<String> list = res.split('\n');
|
||||
list.removeWhere((pack) => pack.isEmpty);
|
||||
apps.addAll(list.map((pack) => pack.trim()).toList());
|
||||
}
|
||||
// TODO(ponces): remove in the future, keep it for now during migration.
|
||||
res = await Root.exec(
|
||||
cmd: 'ls "$_revancedOldDirPath"',
|
||||
);
|
||||
if (res != null) {
|
||||
final List<String> list = res.split('\n');
|
||||
list.removeWhere((pack) => pack.isEmpty);
|
||||
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 {
|
||||
@@ -88,8 +107,12 @@ class RootAPI {
|
||||
await Root.exec(
|
||||
cmd: 'su -mm -c "umount -l $originalFilePath"',
|
||||
);
|
||||
// TODO(ponces): remove in the future, keep it for now during migration.
|
||||
await Root.exec(
|
||||
cmd: 'rm -rf "$_managerDirPath/$packageName"',
|
||||
cmd: 'rm -rf "$_revancedOldDirPath/$packageName"',
|
||||
);
|
||||
await Root.exec(
|
||||
cmd: 'rm -rf "$_revancedDirPath/$packageName"',
|
||||
);
|
||||
await Root.exec(
|
||||
cmd: 'rm -rf "$_serviceDDirPath/$packageName.sh"',
|
||||
@@ -107,13 +130,13 @@ class RootAPI {
|
||||
try {
|
||||
await deleteApp(packageName, originalFilePath);
|
||||
await Root.exec(
|
||||
cmd: 'mkdir -p "$_managerDirPath/$packageName"',
|
||||
cmd: 'mkdir -p "$_revancedDirPath/$packageName"',
|
||||
);
|
||||
await setPermissions(
|
||||
'0755',
|
||||
'shell:shell',
|
||||
'',
|
||||
'$_managerDirPath/$packageName',
|
||||
'$_revancedDirPath/$packageName',
|
||||
);
|
||||
await saveOriginalFilePath(packageName, originalFilePath);
|
||||
await installServiceDScript(packageName);
|
||||
@@ -121,18 +144,24 @@ class RootAPI {
|
||||
await installApk(packageName, patchedFilePath);
|
||||
await mountApk(packageName, originalFilePath);
|
||||
return true;
|
||||
} on Exception {
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
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'
|
||||
await Root.exec(
|
||||
cmd: 'mkdir -p "$_serviceDDirPath"',
|
||||
);
|
||||
final String content = '#!/system/bin/sh\n'
|
||||
'while [ "\$(getprop sys.boot_completed | tr -d \'"\'"\'\\\\r\'"\'"\')" != "1" ]; do sleep 3; done\n'
|
||||
'base_path=$_revancedDirPath/$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';
|
||||
r'[ ! -z $stock_path ] && mount -o bind $base_path $stock_path';
|
||||
final String scriptFilePath = '$_serviceDDirPath/$packageName.sh';
|
||||
await Root.exec(
|
||||
cmd: 'echo \'$content\' > "$scriptFilePath"',
|
||||
);
|
||||
@@ -140,10 +169,13 @@ class RootAPI {
|
||||
}
|
||||
|
||||
Future<void> installPostFsDataScript(String packageName) async {
|
||||
String content = '#!/system/bin/sh\n'
|
||||
await Root.exec(
|
||||
cmd: 'mkdir -p "$_postFsDataDirPath"',
|
||||
);
|
||||
final String content = '#!/system/bin/sh\n'
|
||||
'stock_path=\$(pm path $packageName | grep base | sed \'"\'"\'s/package://g\'"\'"\')\n'
|
||||
'[ ! -z \$stock_path ] && umount -l \$stock_path';
|
||||
String scriptFilePath = '$_postFsDataDirPath/$packageName.sh';
|
||||
r'[ ! -z $stock_path ] && umount -l $stock_path';
|
||||
final String scriptFilePath = '$_postFsDataDirPath/$packageName.sh';
|
||||
await Root.exec(
|
||||
cmd: 'echo \'$content\' > "$scriptFilePath"',
|
||||
);
|
||||
@@ -151,7 +183,7 @@ class RootAPI {
|
||||
}
|
||||
|
||||
Future<void> installApk(String packageName, String patchedFilePath) async {
|
||||
String newPatchedFilePath = '$_managerDirPath/$packageName/base.apk';
|
||||
final String newPatchedFilePath = '$_revancedDirPath/$packageName/base.apk';
|
||||
await Root.exec(
|
||||
cmd: 'cp "$patchedFilePath" "$newPatchedFilePath"',
|
||||
);
|
||||
@@ -164,7 +196,7 @@ class RootAPI {
|
||||
}
|
||||
|
||||
Future<void> mountApk(String packageName, String originalFilePath) async {
|
||||
String newPatchedFilePath = '$_managerDirPath/$packageName/base.apk';
|
||||
final String newPatchedFilePath = '$_revancedDirPath/$packageName/base.apk';
|
||||
await Root.exec(
|
||||
cmd: 'am force-stop "$packageName"',
|
||||
);
|
||||
@@ -177,42 +209,26 @@ class RootAPI {
|
||||
}
|
||||
|
||||
Future<bool> isMounted(String packageName) async {
|
||||
String? res = await Root.exec(
|
||||
final String? res = await Root.exec(
|
||||
cmd: 'cat /proc/mounts | grep $packageName',
|
||||
);
|
||||
return res != null && res.isNotEmpty;
|
||||
}
|
||||
|
||||
Future<String> getOriginalFilePath(
|
||||
String packageName,
|
||||
String originalFilePath,
|
||||
) async {
|
||||
bool isInstalled = await isAppInstalled(packageName);
|
||||
if (isInstalled && await isMounted(packageName)) {
|
||||
originalFilePath = '$_managerDirPath/$packageName/original.apk';
|
||||
await setPermissions(
|
||||
'0644',
|
||||
'shell:shell',
|
||||
'u:object_r:apk_data_file:s0',
|
||||
originalFilePath,
|
||||
);
|
||||
}
|
||||
return originalFilePath;
|
||||
}
|
||||
|
||||
Future<void> saveOriginalFilePath(
|
||||
String packageName,
|
||||
String originalFilePath,
|
||||
) async {
|
||||
String originalRootPath = '$_managerDirPath/$packageName/original.apk';
|
||||
final String originalRootPath =
|
||||
'$_revancedDirPath/$packageName/original.apk';
|
||||
await Root.exec(
|
||||
cmd: 'mkdir -p "$_managerDirPath/$packageName"',
|
||||
cmd: 'mkdir -p "$_revancedDirPath/$packageName"',
|
||||
);
|
||||
await setPermissions(
|
||||
'0755',
|
||||
'shell:shell',
|
||||
'',
|
||||
'$_managerDirPath/$packageName',
|
||||
'$_revancedDirPath/$packageName',
|
||||
);
|
||||
await Root.exec(
|
||||
cmd: 'cp "$originalFilePath" "$originalRootPath"',
|
||||
@@ -224,4 +240,18 @@ class RootAPI {
|
||||
originalFilePath,
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> fileExists(String path) async {
|
||||
try {
|
||||
final String? res = await Root.exec(
|
||||
cmd: 'ls $path',
|
||||
);
|
||||
return res != null && res.isNotEmpty;
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,4 +20,15 @@ class Toast {
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
var lightCustomColorScheme = ColorScheme.fromSeed(
|
||||
seedColor: Colors.blue,
|
||||
brightness: Brightness.light,
|
||||
primary: const Color(0xff1B73E8),
|
||||
);
|
||||
|
||||
@@ -13,7 +12,7 @@ var lightCustomTheme = ThemeData(
|
||||
navigationBarTheme: NavigationBarThemeData(
|
||||
labelTextStyle: MaterialStateProperty.all(
|
||||
TextStyle(
|
||||
color: lightCustomColorScheme.secondary,
|
||||
color: lightCustomColorScheme.onSurface,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
@@ -34,13 +33,12 @@ var darkCustomTheme = ThemeData(
|
||||
navigationBarTheme: NavigationBarThemeData(
|
||||
labelTextStyle: MaterialStateProperty.all(
|
||||
TextStyle(
|
||||
color: darkCustomColorScheme.secondary,
|
||||
color: darkCustomColorScheme.onSurface,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
canvasColor: const Color(0xff1B1A1D),
|
||||
scaffoldBackgroundColor: const Color(0xff1B1A1D),
|
||||
toggleableActiveColor: const Color(0xffA5CAFF),
|
||||
textTheme: GoogleFonts.robotoTextTheme(ThemeData.dark().textTheme),
|
||||
);
|
||||
|
||||
@@ -1,94 +1,114 @@
|
||||
import 'dart:ui';
|
||||
import 'package:dynamic_color/dynamic_color.dart';
|
||||
import 'package:dynamic_themes/dynamic_themes.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.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/services/manager_api.dart';
|
||||
import 'package:revanced_manager/theme.dart';
|
||||
import 'package:stacked_services/stacked_services.dart';
|
||||
|
||||
class DynamicThemeBuilder extends StatelessWidget {
|
||||
final String title;
|
||||
final Widget home;
|
||||
final Iterable<LocalizationsDelegate> localizationsDelegates;
|
||||
|
||||
class DynamicThemeBuilder extends StatefulWidget {
|
||||
const DynamicThemeBuilder({
|
||||
Key? key,
|
||||
required this.title,
|
||||
required this.home,
|
||||
required this.localizationsDelegates,
|
||||
}) : super(key: key);
|
||||
final String title;
|
||||
final Widget home;
|
||||
final Iterable<LocalizationsDelegate> localizationsDelegates;
|
||||
|
||||
@override
|
||||
State<DynamicThemeBuilder> createState() => _DynamicThemeBuilderState();
|
||||
}
|
||||
|
||||
class _DynamicThemeBuilderState extends State<DynamicThemeBuilder> 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
|
||||
Widget build(BuildContext context) {
|
||||
return DynamicColorBuilder(
|
||||
builder: (lightColorScheme, darkColorScheme) {
|
||||
ThemeData lightDynamicTheme = ThemeData(
|
||||
final ThemeData lightDynamicTheme = ThemeData(
|
||||
useMaterial3: true,
|
||||
canvasColor: lightColorScheme?.background,
|
||||
navigationBarTheme: NavigationBarThemeData(
|
||||
backgroundColor: lightColorScheme?.background,
|
||||
indicatorColor: lightColorScheme?.primary.withAlpha(150),
|
||||
labelTextStyle: MaterialStateProperty.all(
|
||||
GoogleFonts.roboto(
|
||||
color: lightColorScheme?.secondary,
|
||||
color: lightColorScheme?.onSurface,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
iconTheme: MaterialStateProperty.all(
|
||||
IconThemeData(
|
||||
color: lightColorScheme?.secondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
scaffoldBackgroundColor: lightColorScheme?.background,
|
||||
colorScheme: lightColorScheme?.harmonized(),
|
||||
toggleableActiveColor: lightColorScheme?.primary,
|
||||
textTheme: GoogleFonts.robotoTextTheme(ThemeData.light().textTheme),
|
||||
);
|
||||
ThemeData darkDynamicTheme = ThemeData(
|
||||
final ThemeData darkDynamicTheme = ThemeData(
|
||||
useMaterial3: true,
|
||||
canvasColor: darkColorScheme?.background,
|
||||
navigationBarTheme: NavigationBarThemeData(
|
||||
backgroundColor: darkColorScheme?.background,
|
||||
indicatorColor: darkColorScheme?.primary.withOpacity(0.4),
|
||||
labelTextStyle: MaterialStateProperty.all(
|
||||
GoogleFonts.roboto(
|
||||
color: darkColorScheme?.secondary,
|
||||
color: darkColorScheme?.onSurface,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
iconTheme: MaterialStateProperty.all(
|
||||
IconThemeData(
|
||||
color: darkColorScheme?.secondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
scaffoldBackgroundColor: darkColorScheme?.background,
|
||||
colorScheme: darkColorScheme?.harmonized(),
|
||||
toggleableActiveColor: darkColorScheme?.primary,
|
||||
textTheme: GoogleFonts.robotoTextTheme(ThemeData.dark().textTheme),
|
||||
);
|
||||
return DynamicTheme(
|
||||
themeCollection: ThemeCollection(
|
||||
themes: {
|
||||
0: lightCustomTheme,
|
||||
1: darkCustomTheme,
|
||||
2: lightDynamicTheme,
|
||||
3: darkDynamicTheme,
|
||||
0: brightness == Brightness.light ? lightCustomTheme : darkCustomTheme,
|
||||
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(
|
||||
debugShowCheckedModeBanner: false,
|
||||
title: title,
|
||||
navigatorKey: StackedService.navigatorKey,
|
||||
onGenerateRoute: StackedRouter().onGenerateRoute,
|
||||
theme: theme,
|
||||
home: home,
|
||||
localizationsDelegates: localizationsDelegates,
|
||||
),
|
||||
debugShowCheckedModeBanner: false,
|
||||
title: widget.title,
|
||||
navigatorKey: StackedService.navigatorKey,
|
||||
onGenerateRoute: StackedRouter().onGenerateRoute,
|
||||
theme: theme,
|
||||
home: widget.home,
|
||||
localizationsDelegates: widget.localizationsDelegates,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/material.dart' hide SearchBar;
|
||||
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/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/search_bar.dart';
|
||||
import 'package:stacked/stacked.dart' hide SkeletonLoader;
|
||||
|
||||
class AppSelectorView extends StatefulWidget {
|
||||
const AppSelectorView({Key? key}) : super(key: key);
|
||||
@@ -19,10 +20,9 @@ class _AppSelectorViewState extends State<AppSelectorView> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ViewModelBuilder<AppSelectorViewModel>.reactive(
|
||||
onModelReady: (model) => model.initialize(),
|
||||
onViewModelReady: (model) => model.initialize(),
|
||||
viewModelBuilder: () => AppSelectorViewModel(),
|
||||
builder: (context, model, child) => Scaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
label: I18nText('appSelectorView.storageButton'),
|
||||
icon: const Icon(Icons.sd_storage),
|
||||
@@ -36,32 +36,28 @@ class _AppSelectorViewState extends State<AppSelectorView> {
|
||||
SliverAppBar(
|
||||
pinned: true,
|
||||
floating: true,
|
||||
snap: false,
|
||||
title: I18nText(
|
||||
'appSelectorView.viewTitle',
|
||||
child: Text(
|
||||
'',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).textTheme.headline6!.color,
|
||||
),
|
||||
),
|
||||
),
|
||||
titleTextStyle: TextStyle(
|
||||
fontSize: 22.0,
|
||||
color: Theme.of(context).textTheme.titleLarge!.color,
|
||||
),
|
||||
leading: IconButton(
|
||||
icon: Icon(
|
||||
Icons.arrow_back,
|
||||
color: Theme.of(context).textTheme.headline6!.color,
|
||||
color: Theme.of(context).textTheme.titleLarge!.color,
|
||||
),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
bottom: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(64.0),
|
||||
preferredSize: const Size.fromHeight(66.0),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 8.0,
|
||||
horizontal: 12.0,
|
||||
),
|
||||
child: SearchBar(
|
||||
showSelectIcon: false,
|
||||
hintText: FlutterI18n.translate(
|
||||
context,
|
||||
'appSelectorView.searchBarHint',
|
||||
@@ -78,26 +74,64 @@ class _AppSelectorViewState extends State<AppSelectorView> {
|
||||
SliverToBoxAdapter(
|
||||
child: model.noApps
|
||||
? 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()
|
||||
: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0)
|
||||
.copyWith(bottom: 80),
|
||||
.copyWith(
|
||||
bottom:
|
||||
MediaQuery.viewPaddingOf(context).bottom + 8.0,
|
||||
),
|
||||
child: Column(
|
||||
children: model
|
||||
.getFilteredApps(_query)
|
||||
.map((app) => InstalledAppItem(
|
||||
children: [
|
||||
...model
|
||||
.getFilteredApps(_query)
|
||||
.map(
|
||||
(app) => InstalledAppItem(
|
||||
name: app.appName,
|
||||
pkgName: app.packageName,
|
||||
icon: app.icon,
|
||||
patchesCount:
|
||||
model.patchesCount(app.packageName),
|
||||
suggestedVersion:
|
||||
model.getSuggestedVersion(
|
||||
app.packageName,
|
||||
),
|
||||
installedVersion: app.versionName!,
|
||||
onTap: () => model.canSelectInstalled(
|
||||
context,
|
||||
app.packageName,
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
...model
|
||||
.getFilteredAppsNames(_query)
|
||||
.map(
|
||||
(app) => NotInstalledAppItem(
|
||||
name: app,
|
||||
patchesCount: model.patchesCount(app),
|
||||
suggestedVersion:
|
||||
model.getSuggestedVersion(app),
|
||||
onTap: () {
|
||||
model.selectApp(app);
|
||||
Navigator.of(context).pop();
|
||||
model.showDownloadToast();
|
||||
},
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
const SizedBox(height: 70.0),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,28 +1,76 @@
|
||||
import 'dart:io';
|
||||
|
||||
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_i18n/flutter_i18n.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/services/manager_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/widgets/shared/custom_material_button.dart';
|
||||
import 'package:revanced_manager/utils/check_for_supported_patch.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
|
||||
class AppSelectorViewModel extends BaseViewModel {
|
||||
final PatcherAPI _patcherAPI = locator<PatcherAPI>();
|
||||
final ManagerAPI _managerAPI = locator<ManagerAPI>();
|
||||
final Toast _toast = locator<Toast>();
|
||||
final List<ApplicationWithIcon> apps = [];
|
||||
List<String> allApps = [];
|
||||
bool noApps = false;
|
||||
bool isRooted = false;
|
||||
int patchesCount(String packageName) {
|
||||
return _patcherAPI.getFilteredPatches(packageName).length;
|
||||
}
|
||||
|
||||
List<Patch> patches = [];
|
||||
|
||||
Future<void> initialize() async {
|
||||
apps.addAll(await _patcherAPI.getFilteredInstalledApps());
|
||||
apps.sort((a, b) => a.appName.compareTo(b.appName));
|
||||
noApps = apps.isEmpty;
|
||||
patches = await _managerAPI.getPatches();
|
||||
isRooted = _managerAPI.isRooted;
|
||||
|
||||
apps.addAll(
|
||||
await _patcherAPI
|
||||
.getFilteredInstalledApps(_managerAPI.areUniversalPatchesEnabled()),
|
||||
);
|
||||
apps.sort(
|
||||
(a, b) => _patcherAPI
|
||||
.getFilteredPatches(b.packageName)
|
||||
.length
|
||||
.compareTo(_patcherAPI.getFilteredPatches(a.packageName).length),
|
||||
);
|
||||
getAllApps();
|
||||
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> selectApp(ApplicationWithIcon application) async {
|
||||
locator<PatcherViewModel>().selectedApp = PatchedApplication(
|
||||
name: application.appName,
|
||||
packageName: application.packageName,
|
||||
@@ -31,19 +79,126 @@ class AppSelectorViewModel extends BaseViewModel {
|
||||
icon: application.icon,
|
||||
patchDate: DateTime.now(),
|
||||
);
|
||||
locator<PatcherViewModel>().selectedPatches.clear();
|
||||
locator<PatcherViewModel>().notifyListeners();
|
||||
await locator<PatcherViewModel>().loadLastSelectedPatches();
|
||||
}
|
||||
|
||||
Future<void> canSelectInstalled(
|
||||
BuildContext context,
|
||||
String packageName,
|
||||
) async {
|
||||
final app =
|
||||
await DeviceApps.getApp(packageName, true) as ApplicationWithIcon?;
|
||||
if (app != null) {
|
||||
if (await checkSplitApk(packageName) && !isRooted) {
|
||||
if (context.mounted) {
|
||||
return showSelectFromStorageDialog(context);
|
||||
}
|
||||
} else if (!await checkSplitApk(packageName) || isRooted) {
|
||||
await selectApp(app);
|
||||
if (context.mounted) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
final List<Option> requiredNullOptions = getNullRequiredOptions(locator<PatcherViewModel>().selectedPatches, packageName);
|
||||
if(requiredNullOptions.isNotEmpty){
|
||||
locator<PatcherViewModel>().showRequiredOptionDialog();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future showSelectFromStorageDialog(BuildContext context) async {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => 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(context).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),
|
||||
CustomMaterialButton(
|
||||
onPressed: () => selectAppFromStorage(context).then(
|
||||
(_) {
|
||||
Navigator.pop(context);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
label: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.sd_card),
|
||||
const SizedBox(width: 10),
|
||||
I18nText('appSelectorView.selectFromStorageButton'),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
CustomMaterialButton(
|
||||
isFilled: false,
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
label: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(width: 10),
|
||||
I18nText('cancelButton'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> selectAppFromStorage(BuildContext context) async {
|
||||
try {
|
||||
FilePickerResult? result = await FilePicker.platform.pickFiles(
|
||||
final FilePickerResult? result = await FilePicker.platform.pickFiles(
|
||||
type: FileType.custom,
|
||||
allowedExtensions: ['apk'],
|
||||
);
|
||||
if (result != null && result.files.single.path != null) {
|
||||
File apkFile = File(result.files.single.path!);
|
||||
ApplicationWithIcon? application = await DeviceApps.getAppFromStorage(
|
||||
final File apkFile = File(result.files.single.path!);
|
||||
final List<String> pathSplit = result.files.single.path!.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,
|
||||
true,
|
||||
) as ApplicationWithIcon?;
|
||||
@@ -57,21 +212,40 @@ class AppSelectorViewModel extends BaseViewModel {
|
||||
patchDate: DateTime.now(),
|
||||
isFromStorage: true,
|
||||
);
|
||||
locator<PatcherViewModel>().selectedPatches.clear();
|
||||
locator<PatcherViewModel>().notifyListeners();
|
||||
locator<PatcherViewModel>().loadLastSelectedPatches();
|
||||
}
|
||||
}
|
||||
} on Exception {
|
||||
_toast.show('appSelectorView.errorMessage');
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
_toast.showBottom('appSelectorView.errorMessage');
|
||||
}
|
||||
}
|
||||
|
||||
List<ApplicationWithIcon> getFilteredApps(String query) {
|
||||
return apps
|
||||
.where((app) =>
|
||||
query.isEmpty ||
|
||||
query.length < 2 ||
|
||||
app.appName.toLowerCase().contains(query.toLowerCase()))
|
||||
.where(
|
||||
(app) =>
|
||||
query.isEmpty ||
|
||||
query.length < 2 ||
|
||||
app.appName.toLowerCase().contains(query.toLowerCase()) ||
|
||||
app.packageName.toLowerCase().contains(query.toLowerCase()),
|
||||
)
|
||||
.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');
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ class ContributorsView extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return ViewModelBuilder<ContributorsViewModel>.reactive(
|
||||
viewModelBuilder: () => ContributorsViewModel(),
|
||||
onModelReady: (model) => model.getContributors(),
|
||||
onViewModelReady: (model) => model.getContributors(),
|
||||
builder: (context, model, child) => Scaffold(
|
||||
body: CustomScrollView(
|
||||
slivers: <Widget>[
|
||||
@@ -23,7 +23,7 @@ class ContributorsView extends StatelessWidget {
|
||||
child: Text(
|
||||
'',
|
||||
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',
|
||||
contributors: model.managerContributors,
|
||||
),
|
||||
SizedBox(height: MediaQuery.viewPaddingOf(context).bottom),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -11,7 +11,7 @@ class ContributorsViewModel extends BaseViewModel {
|
||||
List<dynamic> managerContributors = [];
|
||||
|
||||
Future<void> getContributors() async {
|
||||
Map<String, List<dynamic>> contributors =
|
||||
final Map<String, List<dynamic>> contributors =
|
||||
await _managerAPI.getContributors();
|
||||
patcherContributors = contributors[_managerAPI.defaultPatcherRepo] ?? [];
|
||||
patchesContributors = contributors[_managerAPI.getPatchesRepo()] ?? [];
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_i18n/flutter_i18n.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/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/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:stacked/stacked.dart';
|
||||
|
||||
@@ -18,12 +15,11 @@ class HomeView extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return ViewModelBuilder<HomeViewModel>.reactive(
|
||||
disposeViewModel: false,
|
||||
onModelReady: (model) => model.initialize(context),
|
||||
fireOnViewModelReadyOnce: true,
|
||||
onViewModelReady: (model) => model.initialize(context),
|
||||
viewModelBuilder: () => locator<HomeViewModel>(),
|
||||
builder: (context, model, child) => Scaffold(
|
||||
body: RefreshIndicator(
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
onRefresh: () => model.forceRefresh(context),
|
||||
child: CustomScrollView(
|
||||
slivers: <Widget>[
|
||||
@@ -34,7 +30,7 @@ class HomeView extends StatelessWidget {
|
||||
child: Text(
|
||||
'',
|
||||
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',
|
||||
child: Text(
|
||||
'',
|
||||
style: Theme.of(context).textTheme.headline6!,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
LatestCommitCard(
|
||||
onPressed: () =>
|
||||
model.showUpdateConfirmationDialog(context),
|
||||
),
|
||||
LatestCommitCard(model: model, parentContext: context),
|
||||
const SizedBox(height: 23),
|
||||
I18nText(
|
||||
'homeView.patchedSubtitle',
|
||||
child: Text(
|
||||
'',
|
||||
style: Theme.of(context).textTheme.headline6!,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
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(),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
InstalledAppsCard(),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,54 +1,86 @@
|
||||
// ignore_for_file: use_build_context_synchronously
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:app_installer/app_installer.dart';
|
||||
import 'package:cross_connectivity/cross_connectivity.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_i18n/flutter_i18n.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:install_plugin/install_plugin.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:revanced_manager/app/app.locator.dart';
|
||||
import 'package:revanced_manager/app/app.router.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/patcher_api.dart';
|
||||
import 'package:revanced_manager/services/revanced_api.dart';
|
||||
import 'package:revanced_manager/services/toast.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/widgets/homeView/update_confirmation_dialog.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
import 'package:stacked_services/stacked_services.dart';
|
||||
import 'package:timezone/timezone.dart' as tz;
|
||||
|
||||
@lazySingleton
|
||||
class HomeViewModel extends BaseViewModel {
|
||||
final NavigationService _navigationService = locator<NavigationService>();
|
||||
final ManagerAPI _managerAPI = locator<ManagerAPI>();
|
||||
final PatcherAPI _patcherAPI = locator<PatcherAPI>();
|
||||
final GithubAPI _githubAPI = locator<GithubAPI>();
|
||||
final RevancedAPI _revancedAPI = locator<RevancedAPI>();
|
||||
final Toast _toast = locator<Toast>();
|
||||
final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
|
||||
DateTime? _lastUpdate;
|
||||
bool showUpdatableApps = true;
|
||||
bool showUpdatableApps = false;
|
||||
List<PatchedApplication> patchedInstalledApps = [];
|
||||
List<PatchedApplication> patchedUpdatableApps = [];
|
||||
String? _latestManagerVersion = '';
|
||||
File? downloadedApk;
|
||||
|
||||
Future<void> initialize(BuildContext context) async {
|
||||
_latestManagerVersion = await _managerAPI.getLatestManagerVersion();
|
||||
if (!_managerAPI.getPatchesConsent()) {
|
||||
await showPatchesConsent(context);
|
||||
}
|
||||
await _patcherAPI.initialize();
|
||||
await flutterLocalNotificationsPlugin.initialize(
|
||||
const InitializationSettings(
|
||||
android: AndroidInitializationSettings('ic_notification'),
|
||||
),
|
||||
onSelectNotification: (p) =>
|
||||
DeviceApps.openApp('app.revanced.manager.flutter'),
|
||||
onDidReceiveNotificationResponse: (response) async {
|
||||
if (response.id == 0) {
|
||||
_toast.showBottom('homeView.installingMessage');
|
||||
final File? managerApk = await _managerAPI.downloadManager();
|
||||
if (managerApk != null) {
|
||||
await InstallPlugin.installApk(managerApk.path);
|
||||
} else {
|
||||
_toast.showBottom('homeView.errorDownloadMessage');
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
flutterLocalNotificationsPlugin
|
||||
.resolvePlatformSpecificImplementation<
|
||||
AndroidFlutterLocalNotificationsPlugin>()
|
||||
?.requestPermission();
|
||||
bool isConnected = await Connectivity().checkConnection();
|
||||
final bool isConnected = await Connectivity().checkConnection();
|
||||
if (!isConnected) {
|
||||
_toast.show('homeView.noConnection');
|
||||
_toast.showBottom('homeView.noConnection');
|
||||
}
|
||||
_getPatchedApps();
|
||||
final NotificationAppLaunchDetails? notificationAppLaunchDetails =
|
||||
await flutterLocalNotificationsPlugin.getNotificationAppLaunchDetails();
|
||||
if (notificationAppLaunchDetails?.didNotificationLaunchApp ?? false) {
|
||||
_toast.showBottom('homeView.installingMessage');
|
||||
final File? managerApk = await _managerAPI.downloadManager();
|
||||
if (managerApk != null) {
|
||||
await InstallPlugin.installApk(managerApk.path);
|
||||
} else {
|
||||
_toast.showBottom('homeView.errorDownloadMessage');
|
||||
}
|
||||
}
|
||||
|
||||
_managerAPI.reAssessSavedApps().then((_) => _getPatchedApps());
|
||||
}
|
||||
|
||||
@@ -64,7 +96,7 @@ class HomeViewModel extends BaseViewModel {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void navigateToPatcher(PatchedApplication app) async {
|
||||
Future<void> navigateToPatcher(PatchedApplication app) async {
|
||||
locator<PatcherViewModel>().selectedApp = app;
|
||||
locator<PatcherViewModel>().selectedPatches =
|
||||
await _patcherAPI.getAppliedPatches(app.appliedPatches);
|
||||
@@ -73,112 +105,366 @@ class HomeViewModel extends BaseViewModel {
|
||||
}
|
||||
|
||||
void _getPatchedApps() {
|
||||
patchedInstalledApps = _managerAPI
|
||||
.getPatchedApps()
|
||||
.where((app) => app.hasUpdates == false)
|
||||
.toList();
|
||||
patchedUpdatableApps = _managerAPI
|
||||
.getPatchedApps()
|
||||
.where((app) => app.hasUpdates == true)
|
||||
.toList();
|
||||
patchedInstalledApps = _managerAPI.getPatchedApps().toList();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<bool> hasManagerUpdates() async {
|
||||
String? latestVersion = await _managerAPI.getLatestManagerVersion();
|
||||
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) {
|
||||
try {
|
||||
int latestVersionInt =
|
||||
final int latestVersionInt =
|
||||
int.parse(latestVersion.replaceAll(RegExp('[^0-9]'), ''));
|
||||
int currentVersionInt =
|
||||
final int currentVersionInt =
|
||||
int.parse(currentVersion.replaceAll(RegExp('[^0-9]'), ''));
|
||||
return latestVersionInt > currentVersionInt;
|
||||
} on Exception {
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<void> updateManager(BuildContext context) async {
|
||||
Future<File?> downloadManager() async {
|
||||
try {
|
||||
_toast.show('homeView.downloadingMessage');
|
||||
File? managerApk = await _managerAPI.downloadManager();
|
||||
if (managerApk != null) {
|
||||
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.show('homeView.installingMessage');
|
||||
await AppInstaller.installApk(managerApk.path);
|
||||
} else {
|
||||
_toast.show('homeView.errorDownloadMessage');
|
||||
final response = await _revancedAPI.downloadManager();
|
||||
final bytes = await response!.readAsBytes();
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
final tempFile = File('${tempDir.path}/revanced-manager.apk');
|
||||
await tempFile.writeAsBytes(bytes);
|
||||
return tempFile;
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
} on Exception {
|
||||
_toast.show('homeView.errorInstallMessage');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> showUpdateConfirmationDialog(BuildContext parentContext) async {
|
||||
return showDialog(
|
||||
context: parentContext,
|
||||
Future<void> showPatchesConsent(BuildContext context) async {
|
||||
final ValueNotifier<bool> autoUpdate = ValueNotifier(true);
|
||||
await showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => AlertDialog(
|
||||
title: I18nText('homeView.updateDialogTitle'),
|
||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
content: I18nText('homeView.updateDialogText'),
|
||||
actions: <Widget>[
|
||||
title: const Text('Download ReVanced Patches?'),
|
||||
content: ValueListenableBuilder(
|
||||
valueListenable: autoUpdate,
|
||||
builder: (context, value, child) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
I18nText(
|
||||
'homeView.patchesConsentDialogText',
|
||||
child: Text(
|
||||
'',
|
||||
style: TextStyle(
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
CheckboxListTile(
|
||||
value: value,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: I18nText(
|
||||
'homeView.patchesConsentDialogText3',
|
||||
),
|
||||
subtitle: I18nText(
|
||||
'homeView.patchesConsentDialogText3Sub',
|
||||
),
|
||||
onChanged: (selected) {
|
||||
autoUpdate.value = selected!;
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
CustomMaterialButton(
|
||||
isFilled: false,
|
||||
label: I18nText('noButton'),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
onPressed: () async {
|
||||
await _managerAPI.setPatchesConsent(false);
|
||||
SystemNavigator.pop();
|
||||
},
|
||||
label: I18nText('quitButton'),
|
||||
),
|
||||
CustomMaterialButton(
|
||||
label: I18nText('yesButton'),
|
||||
onPressed: () {
|
||||
onPressed: () async {
|
||||
await _managerAPI.setPatchesConsent(true);
|
||||
await _managerAPI.setPatchesAutoUpdate(autoUpdate.value);
|
||||
Navigator.of(context).pop();
|
||||
updateManager(parentContext);
|
||||
},
|
||||
)
|
||||
label: I18nText('okButton'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<String?> getLatestPatcherReleaseTime() async {
|
||||
return _managerAPI.getLatestPatcherReleaseTime();
|
||||
Future<void> updatePatches(BuildContext context) async {
|
||||
_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(
|
||||
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: CustomMaterialButton(
|
||||
label: I18nText('cancelButton'),
|
||||
onPressed: () {
|
||||
_revancedAPI.disposeManagerUpdateProgress();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
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: CustomMaterialButton(
|
||||
isFilled: false,
|
||||
label: I18nText('cancelButton'),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8.0),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: CustomMaterialButton(
|
||||
label: I18nText('updateButton'),
|
||||
onPressed: () async {
|
||||
await InstallPlugin.installApk(
|
||||
downloadedApk!.path,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
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 InstallPlugin.installApk(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();
|
||||
}
|
||||
|
||||
Future<void> forceRefresh(BuildContext context) async {
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
if (_lastUpdate == null ||
|
||||
_lastUpdate!.difference(DateTime.now()).inSeconds > 60) {
|
||||
_managerAPI.clearAllData();
|
||||
}
|
||||
_managerAPI.clearAllData();
|
||||
_toast.showBottom('homeView.refreshSuccess');
|
||||
initialize(context);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_i18n/flutter_i18n.dart';
|
||||
import 'package:google_fonts/google_fonts.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/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:stacked/stacked.dart';
|
||||
|
||||
@@ -15,12 +13,64 @@ class InstallerView extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ViewModelBuilder<InstallerViewModel>.reactive(
|
||||
onModelReady: (model) => model.initialize(context),
|
||||
onViewModelReady: (model) => model.initialize(context),
|
||||
viewModelBuilder: () => InstallerViewModel(),
|
||||
builder: (context, model, child) => WillPopScope(
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
bottom: false,
|
||||
child: Scaffold(
|
||||
floatingActionButton: Visibility(
|
||||
visible: !model.isPatching && !model.hasErrors,
|
||||
child: FloatingActionButton.extended(
|
||||
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(
|
||||
controller: model.scrollController,
|
||||
slivers: <Widget>[
|
||||
@@ -28,43 +78,16 @@ class InstallerView extends StatelessWidget {
|
||||
title: Text(
|
||||
model.headerLogs,
|
||||
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),
|
||||
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(
|
||||
preferredSize: const Size(double.infinity, 1.0),
|
||||
child:
|
||||
GradientProgressIndicator(progress: model.progress!)),
|
||||
preferredSize: const Size(double.infinity, 1.0),
|
||||
child: GradientProgressIndicator(progress: model.progress),
|
||||
),
|
||||
),
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
@@ -84,66 +107,6 @@ 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// ignore_for_file: use_build_context_synchronously
|
||||
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_background/flutter_background.dart';
|
||||
@@ -14,6 +15,8 @@ import 'package:revanced_manager/services/root_api.dart';
|
||||
import 'package:revanced_manager/services/toast.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:wakelock/wakelock.dart';
|
||||
|
||||
@@ -28,6 +31,7 @@ class InstallerViewModel extends BaseViewModel {
|
||||
'app.revanced.manager.flutter/installer',
|
||||
);
|
||||
final ScrollController scrollController = ScrollController();
|
||||
final ScreenshotCallback screenshotCallback = ScreenshotCallback();
|
||||
double? progress = 0.0;
|
||||
String logs = '';
|
||||
String headerLogs = '';
|
||||
@@ -35,6 +39,9 @@ class InstallerViewModel extends BaseViewModel {
|
||||
bool isPatching = true;
|
||||
bool isInstalled = false;
|
||||
bool hasErrors = false;
|
||||
bool isCanceled = false;
|
||||
bool cancel = false;
|
||||
bool showPopupScreenshotWarning = true;
|
||||
|
||||
Future<void> initialize(BuildContext context) async {
|
||||
isRooted = await _rootAPI.isRooted();
|
||||
@@ -50,17 +57,23 @@ class InstallerViewModel extends BaseViewModel {
|
||||
context,
|
||||
'installerView.notificationText',
|
||||
),
|
||||
notificationImportance: AndroidNotificationImportance.Default,
|
||||
notificationIcon: const AndroidResource(
|
||||
name: 'ic_notification',
|
||||
defType: 'drawable',
|
||||
),
|
||||
),
|
||||
).then((value) => FlutterBackground.enableBackgroundExecution());
|
||||
} on Exception {
|
||||
// ignore
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
} // ignore
|
||||
}
|
||||
}
|
||||
screenshotCallback.addListener(() {
|
||||
if (showPopupScreenshotWarning) {
|
||||
showPopupScreenshotWarning = false;
|
||||
screenshotDetected(context);
|
||||
}
|
||||
});
|
||||
await Wakelock.enable();
|
||||
await handlePlatformChannelMethods();
|
||||
await runPatcher();
|
||||
@@ -71,10 +84,10 @@ class InstallerViewModel extends BaseViewModel {
|
||||
switch (call.method) {
|
||||
case 'update':
|
||||
if (call.arguments != null) {
|
||||
Map<dynamic, dynamic> arguments = call.arguments;
|
||||
double progress = arguments['progress'];
|
||||
String header = arguments['header'];
|
||||
String log = arguments['log'];
|
||||
final Map<dynamic, dynamic> arguments = call.arguments;
|
||||
final double progress = arguments['progress'];
|
||||
final String header = arguments['header'];
|
||||
final String log = arguments['log'];
|
||||
update(progress, header, log);
|
||||
}
|
||||
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) {
|
||||
progress = value;
|
||||
}
|
||||
@@ -94,6 +107,11 @@ class InstallerViewModel extends BaseViewModel {
|
||||
} else if (value == 1.0) {
|
||||
isPatching = false;
|
||||
hasErrors = false;
|
||||
await _managerAPI.savePatches(
|
||||
_patcherAPI.getFilteredPatches(_app.packageName),
|
||||
_app.packageName,
|
||||
);
|
||||
await _managerAPI.setUsedPatches(_patches, _app.packageName);
|
||||
} else if (value == -100.0) {
|
||||
isPatching = false;
|
||||
hasErrors = true;
|
||||
@@ -121,64 +139,213 @@ class InstallerViewModel extends BaseViewModel {
|
||||
}
|
||||
|
||||
Future<void> runPatcher() async {
|
||||
update(0.0, 'Initializing...', 'Initializing installer');
|
||||
if (_patches.isNotEmpty) {
|
||||
try {
|
||||
update(0.1, '', 'Creating working directory');
|
||||
await _patcherAPI.runPatcher(
|
||||
_app.packageName,
|
||||
_app.apkFilePath,
|
||||
_patches,
|
||||
);
|
||||
} catch (e) {
|
||||
update(
|
||||
-100.0,
|
||||
'Aborting...',
|
||||
'An error occurred! Aborting\nError:\n$e',
|
||||
);
|
||||
}
|
||||
} else {
|
||||
update(-100.0, 'Aborting...', 'No app or patches selected! Aborting');
|
||||
}
|
||||
if (FlutterBackground.isBackgroundExecutionEnabled) {
|
||||
try {
|
||||
FlutterBackground.disableBackgroundExecution();
|
||||
} on Exception {
|
||||
// ignore
|
||||
try {
|
||||
await _patcherAPI.runPatcher(
|
||||
_app.packageName,
|
||||
_app.apkFilePath,
|
||||
_patches,
|
||||
);
|
||||
} on Exception catch (e) {
|
||||
update(
|
||||
-100.0,
|
||||
'Failed...',
|
||||
'Something went wrong:\n$e',
|
||||
);
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
|
||||
// Necessary to reset the state of patches so that they
|
||||
// can be reloaded again.
|
||||
_managerAPI.patches.clear();
|
||||
await _patcherAPI.loadPatches();
|
||||
|
||||
try {
|
||||
if (FlutterBackground.isBackgroundExecutionEnabled) {
|
||||
try {
|
||||
FlutterBackground.disableBackgroundExecution();
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
} // ignore
|
||||
}
|
||||
}
|
||||
await Wakelock.disable();
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
await Wakelock.disable();
|
||||
}
|
||||
|
||||
void installResult(BuildContext context, bool installAsRoot) async {
|
||||
_app.isRooted = installAsRoot;
|
||||
bool hasMicroG = _patches.any((p) => p.name.endsWith('microg-support'));
|
||||
bool rootMicroG = installAsRoot && hasMicroG;
|
||||
bool rootFromStorage = installAsRoot && _app.isFromStorage;
|
||||
bool ytWithoutRootMicroG =
|
||||
!installAsRoot && !hasMicroG && _app.packageName.contains('youtube');
|
||||
if (rootMicroG || rootFromStorage || ytWithoutRootMicroG) {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: I18nText('installerView.installErrorDialogTitle'),
|
||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
content: I18nText(
|
||||
rootMicroG
|
||||
? 'installerView.installErrorDialogText1'
|
||||
: rootFromStorage
|
||||
? 'installerView.installErrorDialogText3'
|
||||
: 'installerView.installErrorDialogText2',
|
||||
Future<void> copyLogs() async {
|
||||
final info = await AboutInfo.getInfo();
|
||||
|
||||
final formattedLogs = [
|
||||
'- Device Info',
|
||||
'ReVanced Manager: ${info['version']}',
|
||||
'Build: ${info['flavor']}',
|
||||
'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}',
|
||||
'Patches version: ${_managerAPI.patchesVersion}',
|
||||
'Patches: ${_patches.map((p) => p.name + (p.options.isEmpty ? '' : ' [${p.options.map((o) => '${o.title}: ${o.value}').join(", ")}]')).toList().join(", ")}',
|
||||
|
||||
'\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',
|
||||
logs,
|
||||
];
|
||||
|
||||
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',
|
||||
),
|
||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
icon: const Icon(Icons.warning),
|
||||
content: SingleChildScrollView(
|
||||
child: I18nText('installerView.screenshotDetected'),
|
||||
),
|
||||
actions: <Widget>[
|
||||
CustomMaterialButton(
|
||||
isFilled: false,
|
||||
label: I18nText('noButton'),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
actions: <Widget>[
|
||||
CustomMaterialButton(
|
||||
label: I18nText('yesButton'),
|
||||
onPressed: () {
|
||||
copyLogs();
|
||||
showPopupScreenshotWarning = true;
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> installTypeDialog(BuildContext context) async {
|
||||
final ValueNotifier<int> installType = ValueNotifier(0);
|
||||
if (isRooted) {
|
||||
await showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => AlertDialog(
|
||||
title: I18nText(
|
||||
'installerView.installType',
|
||||
),
|
||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
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!;
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
CustomMaterialButton(
|
||||
label: I18nText('okButton'),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
)
|
||||
label: I18nText('cancelButton'),
|
||||
isFilled: false,
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
CustomMaterialButton(
|
||||
label: I18nText('installerView.installButton'),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
installResult(context, installType.value == 1);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
installResult(context, false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> stopPatcher() async {
|
||||
try {
|
||||
isCanceled = true;
|
||||
update(0.5, 'Canceling...', 'Canceling patching process');
|
||||
await _patcherAPI.stopPatcher();
|
||||
update(-100.0, 'Canceled...', 'Press back to exit');
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> installResult(BuildContext context, bool installAsRoot) async {
|
||||
try {
|
||||
_app.isRooted = installAsRoot;
|
||||
update(
|
||||
1.0,
|
||||
'Installing...',
|
||||
@@ -188,58 +355,88 @@ class InstallerViewModel extends BaseViewModel {
|
||||
);
|
||||
isInstalled = await _patcherAPI.installPatchedFile(_app);
|
||||
if (isInstalled) {
|
||||
update(1.0, 'Installed!', 'Installed!');
|
||||
_app.isFromStorage = false;
|
||||
_app.patchDate = DateTime.now();
|
||||
_app.appliedPatches = _patches.map((p) => p.name).toList();
|
||||
if (hasMicroG) {
|
||||
_app.name += ' ReVanced';
|
||||
_app.packageName = _app.packageName.replaceFirst(
|
||||
'com.google.',
|
||||
'app.revanced.',
|
||||
);
|
||||
|
||||
// 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);
|
||||
|
||||
update(1.0, 'Installed!', 'Installed!');
|
||||
} else {
|
||||
// TODO(aabed): Show error message.
|
||||
}
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void shareResult() {
|
||||
_patcherAPI.sharePatchedFile(_app.name, _app.version);
|
||||
}
|
||||
|
||||
void shareLog() {
|
||||
_patcherAPI.sharePatcherLog(logs);
|
||||
void exportResult() {
|
||||
try {
|
||||
_patcherAPI.exportPatchedFile(_app.name, _app.version);
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> cleanPatcher() async {
|
||||
_patcherAPI.cleanPatcher();
|
||||
locator<PatcherViewModel>().selectedApp = null;
|
||||
locator<PatcherViewModel>().selectedPatches.clear();
|
||||
locator<PatcherViewModel>().notifyListeners();
|
||||
try {
|
||||
_patcherAPI.cleanPatcher();
|
||||
locator<PatcherViewModel>().selectedApp = null;
|
||||
locator<PatcherViewModel>().selectedPatches.clear();
|
||||
locator<PatcherViewModel>().notifyListeners();
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void openApp() {
|
||||
DeviceApps.openApp(_app.packageName);
|
||||
}
|
||||
|
||||
void onMenuSelection(int value) {
|
||||
void onButtonPressed(int value) {
|
||||
switch (value) {
|
||||
case 0:
|
||||
shareResult();
|
||||
exportResult();
|
||||
break;
|
||||
case 1:
|
||||
shareLog();
|
||||
copyLogs();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> onWillPop(BuildContext context) async {
|
||||
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;
|
||||
}
|
||||
cleanPatcher();
|
||||
if (!cancel) {
|
||||
cleanPatcher();
|
||||
} else {
|
||||
_patcherAPI.cleanPatcher();
|
||||
}
|
||||
screenshotCallback.dispose();
|
||||
Navigator.of(context).pop();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -11,60 +11,70 @@ class NavigationView extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ViewModelBuilder<NavigationViewModel>.reactive(
|
||||
onModelReady: (model) => model.initialize(context),
|
||||
onViewModelReady: (model) => model.initialize(context),
|
||||
viewModelBuilder: () => locator<NavigationViewModel>(),
|
||||
builder: (context, model, child) => Scaffold(
|
||||
body: PageTransitionSwitcher(
|
||||
duration: const Duration(milliseconds: 400),
|
||||
transitionBuilder: (
|
||||
Widget child,
|
||||
Animation<double> animation,
|
||||
Animation<double> secondaryAnimation,
|
||||
) {
|
||||
return FadeThroughTransition(
|
||||
animation: animation,
|
||||
secondaryAnimation: secondaryAnimation,
|
||||
fillColor: Theme.of(context).colorScheme.surface,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: model.getViewForIndex(model.currentIndex),
|
||||
),
|
||||
bottomNavigationBar: NavigationBar(
|
||||
onDestinationSelected: model.setIndex,
|
||||
selectedIndex: model.currentIndex,
|
||||
destinations: <Widget>[
|
||||
NavigationDestination(
|
||||
icon: model.isIndexSelected(0)
|
||||
? const Icon(Icons.dashboard)
|
||||
: const Icon(Icons.dashboard_outlined),
|
||||
label: FlutterI18n.translate(
|
||||
context,
|
||||
'navigationView.dashboardTab',
|
||||
builder: (context, model, child) => WillPopScope(
|
||||
onWillPop: () async {
|
||||
if (model.currentIndex == 0) {
|
||||
return true;
|
||||
} else {
|
||||
model.setIndex(0);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
child: Scaffold(
|
||||
body: PageTransitionSwitcher(
|
||||
duration: const Duration(milliseconds: 400),
|
||||
transitionBuilder: (
|
||||
Widget child,
|
||||
Animation<double> animation,
|
||||
Animation<double> secondaryAnimation,
|
||||
) {
|
||||
return FadeThroughTransition(
|
||||
animation: animation,
|
||||
secondaryAnimation: secondaryAnimation,
|
||||
fillColor: Theme.of(context).colorScheme.surface,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: model.getViewForIndex(model.currentIndex),
|
||||
),
|
||||
bottomNavigationBar: NavigationBar(
|
||||
onDestinationSelected: model.setIndex,
|
||||
selectedIndex: model.currentIndex,
|
||||
destinations: <Widget>[
|
||||
NavigationDestination(
|
||||
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)
|
||||
? const Icon(Icons.build)
|
||||
: const Icon(Icons.build_outlined),
|
||||
label: FlutterI18n.translate(
|
||||
context,
|
||||
'navigationView.patcherTab',
|
||||
NavigationDestination(
|
||||
icon: model.isIndexSelected(1)
|
||||
? const Icon(Icons.build)
|
||||
: const Icon(Icons.build_outlined),
|
||||
label: FlutterI18n.translate(
|
||||
context,
|
||||
'navigationView.patcherTab',
|
||||
),
|
||||
tooltip: '',
|
||||
),
|
||||
tooltip: '',
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: model.isIndexSelected(2)
|
||||
? const Icon(Icons.settings)
|
||||
: const Icon(Icons.settings_outlined),
|
||||
label: FlutterI18n.translate(
|
||||
context,
|
||||
'navigationView.settingsTab',
|
||||
NavigationDestination(
|
||||
icon: model.isIndexSelected(2)
|
||||
? const Icon(Icons.settings)
|
||||
: const Icon(Icons.settings_outlined),
|
||||
label: FlutterI18n.translate(
|
||||
context,
|
||||
'navigationView.settingsTab',
|
||||
),
|
||||
tooltip: '',
|
||||
),
|
||||
tooltip: '',
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// 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:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
@@ -15,23 +16,37 @@ import 'package:stacked/stacked.dart';
|
||||
|
||||
@lazySingleton
|
||||
class NavigationViewModel extends IndexTrackingViewModel {
|
||||
void initialize(BuildContext context) async {
|
||||
Future<void> initialize(BuildContext context) async {
|
||||
locator<Toast>().initialize(context);
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
final SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
|
||||
if (prefs.getBool('permissionsRequested') == null) {
|
||||
await Permission.storage.request();
|
||||
await prefs.setBool('permissionsRequested', true);
|
||||
RootAPI().hasRootPermissions().then(
|
||||
await RootAPI().hasRootPermissions().then(
|
||||
(value) => Permission.requestInstallPackages.request().then(
|
||||
(value) => Permission.ignoreBatteryOptimizations.request(),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (prefs.getBool('useDarkTheme') == null) {
|
||||
bool isDark =
|
||||
MediaQuery.of(context).platformBrightness != Brightness.light;
|
||||
await prefs.setBool('useDarkTheme', isDark);
|
||||
await DynamicTheme.of(context)!.setTheme(isDark ? 1 : 0);
|
||||
|
||||
final dynamicTheme = DynamicTheme.of(context)!;
|
||||
if (prefs.getInt('themeMode') == null) {
|
||||
await prefs.setInt('themeMode', 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.setSystemUIOverlayStyle(
|
||||
SystemUiOverlayStyle(
|
||||
|
||||
133
lib/ui/views/patch_options/patch_options_view.dart
Normal file
@@ -0,0 +1,133 @@
|
||||
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/custom_material_button.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: FloatingActionButton.extended(
|
||||
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.optionClassType == 'StringPatchOption' ||
|
||||
option.optionClassType == 'IntPatchOption')
|
||||
IntAndStringPatchOption(
|
||||
patchOption: option,
|
||||
removeOption: (option) {
|
||||
model.removeOption(option);
|
||||
},
|
||||
onChanged: (value, option) {
|
||||
model.modifyOptions(value, option);
|
||||
},
|
||||
)
|
||||
else if (option.optionClassType == 'BooleanPatchOption')
|
||||
BooleanPatchOption(
|
||||
patchOption: option,
|
||||
removeOption: (option) {
|
||||
model.removeOption(option);
|
||||
},
|
||||
onChanged: (value, option) {
|
||||
model.modifyOptions(value, option);
|
||||
},
|
||||
)
|
||||
else if (option.optionClassType ==
|
||||
'StringListPatchOption' ||
|
||||
option.optionClassType == 'IntListPatchOption' ||
|
||||
option.optionClassType == 'LongListPatchOption')
|
||||
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,
|
||||
),
|
||||
CustomMaterialButton(
|
||||
onPressed: () {
|
||||
model.showAddOptionDialog(context);
|
||||
},
|
||||
label: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.add),
|
||||
I18nText('patchOptionsView.addOptions'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(
|
||||
height: 80,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
258
lib/ui/views/patch_options/patch_options_viewmodel.dart
Normal file
@@ -0,0 +1,258 @@
|
||||
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:revanced_manager/ui/widgets/shared/custom_material_button.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),
|
||||
)
|
||||
.toList(),
|
||||
];
|
||||
} else {
|
||||
visibleOptions = [
|
||||
...options.where((option) => option.required).toList(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
optionClassType: option.optionClassType,
|
||||
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,
|
||||
optionClassType: option.optionClassType,
|
||||
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(
|
||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
title: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
I18nText(
|
||||
'patchOptionsView.addOptions',
|
||||
),
|
||||
Text(
|
||||
'',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Theme.of(context).colorScheme.onSecondaryContainer,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
CustomMaterialButton(
|
||||
label: I18nText('cancelButton'),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
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: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
e.title,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
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(
|
||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
title: I18nText('notice'),
|
||||
actions: [
|
||||
CustomMaterialButton(
|
||||
isFilled: false,
|
||||
label: I18nText(
|
||||
'patchOptionsView.deselectPatch',
|
||||
),
|
||||
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);
|
||||
}
|
||||
},
|
||||
),
|
||||
CustomMaterialButton(
|
||||
label: I18nText('okButton'),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
content: I18nText(
|
||||
'patchOptionsView.requiredOptionNull',
|
||||
translationParams: {
|
||||
'options': optionsTitles.join('\n'),
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -22,7 +22,14 @@ class PatcherView extends StatelessWidget {
|
||||
child: FloatingActionButton.extended(
|
||||
label: I18nText('patcherView.patchButton'),
|
||||
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(
|
||||
@@ -34,7 +41,7 @@ class PatcherView extends StatelessWidget {
|
||||
child: Text(
|
||||
'',
|
||||
style: GoogleFonts.inter(
|
||||
color: Theme.of(context).textTheme.headline6!.color,
|
||||
color: Theme.of(context).textTheme.titleLarge!.color,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -45,7 +52,10 @@ class PatcherView extends StatelessWidget {
|
||||
delegate: SliverChildListDelegate.fixed(
|
||||
<Widget>[
|
||||
AppSelectorCard(
|
||||
onPressed: () => model.navigateToAppSelector(),
|
||||
onPressed: () => {
|
||||
model.navigateToAppSelector(),
|
||||
model.ctx = context,
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Opacity(
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// ignore_for_file: use_build_context_synchronously
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_i18n/flutter_i18n.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
@@ -8,6 +10,8 @@ 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/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_services/stacked_services.dart';
|
||||
|
||||
@@ -17,7 +21,9 @@ class PatcherViewModel extends BaseViewModel {
|
||||
final ManagerAPI _managerAPI = locator<ManagerAPI>();
|
||||
final PatcherAPI _patcherAPI = locator<PatcherAPI>();
|
||||
PatchedApplication? selectedApp;
|
||||
BuildContext? ctx;
|
||||
List<Patch> selectedPatches = [];
|
||||
List<String> removedPatches = [];
|
||||
|
||||
void navigateToAppSelector() {
|
||||
_navigationService.navigateTo(Routes.appSelectorView);
|
||||
@@ -39,44 +45,107 @@ class PatcherViewModel extends BaseViewModel {
|
||||
return selectedApp == null;
|
||||
}
|
||||
|
||||
Future<bool> isValidPatchConfig() async {
|
||||
bool needsResourcePatching = await _patcherAPI.needsResourcePatching(
|
||||
selectedPatches,
|
||||
);
|
||||
if (needsResourcePatching && selectedApp != null) {
|
||||
bool isSplit = await _managerAPI.isSplitApk(selectedApp!);
|
||||
return !isSplit;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<void> showPatchConfirmationDialog(BuildContext context) async {
|
||||
bool isValid = await isValidPatchConfig();
|
||||
if (isValid) {
|
||||
navigateToInstaller();
|
||||
} else {
|
||||
return showDialog(
|
||||
bool showRemovedPatchesDialog(BuildContext context) {
|
||||
if (removedPatches.isNotEmpty) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: I18nText('patcherView.patchDialogTitle'),
|
||||
title: I18nText('notice'),
|
||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
content: I18nText('patcherView.patchDialogText'),
|
||||
content: I18nText(
|
||||
'patcherView.removedPatchesWarningDialogText',
|
||||
translationParams: {'patches': removedPatches.join('\n')},
|
||||
),
|
||||
actions: <Widget>[
|
||||
CustomMaterialButton(
|
||||
isFilled: false,
|
||||
label: I18nText('noButton'),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
CustomMaterialButton(
|
||||
label: I18nText('yesButton'),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
navigateToInstaller();
|
||||
showArmv7WarningDialog(context);
|
||||
},
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool checkRequiredPatchOption(BuildContext context) {
|
||||
if (getNullRequiredOptions(selectedPatches, selectedApp!.packageName)
|
||||
.isNotEmpty) {
|
||||
showRequiredOptionDialog(context);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void showRequiredOptionDialog([context]) {
|
||||
showDialog(
|
||||
context: context ?? ctx,
|
||||
builder: (context) => AlertDialog(
|
||||
title: I18nText('notice'),
|
||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
content: I18nText('patcherView.requiredOptionDialogText'),
|
||||
actions: <Widget>[
|
||||
CustomMaterialButton(
|
||||
isFilled: false,
|
||||
label: I18nText('cancelButton'),
|
||||
onPressed: () => {
|
||||
Navigator.of(context).pop(),
|
||||
},
|
||||
),
|
||||
CustomMaterialButton(
|
||||
label: I18nText('okButton'),
|
||||
onPressed: () => {
|
||||
Navigator.pop(context),
|
||||
navigateToPatchesSelector(),
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: I18nText('warning'),
|
||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
content: I18nText('patcherView.armv7WarningDialogText'),
|
||||
actions: <Widget>[
|
||||
CustomMaterialButton(
|
||||
label: I18nText('noButton'),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
CustomMaterialButton(
|
||||
label: I18nText('yesButton'),
|
||||
isFilled: false,
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
navigateToInstaller();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
navigateToInstaller();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,23 +157,58 @@ class PatcherViewModel extends BaseViewModel {
|
||||
return text;
|
||||
}
|
||||
|
||||
String getRecommendedVersionString(BuildContext context) {
|
||||
String recommendedVersion =
|
||||
_patcherAPI.getRecommendedVersion(selectedApp!.packageName);
|
||||
if (recommendedVersion.isEmpty) {
|
||||
recommendedVersion = FlutterI18n.translate(
|
||||
String getSuggestedVersionString(BuildContext context) {
|
||||
String suggestedVersion =
|
||||
_patcherAPI.getSuggestedVersion(selectedApp!.packageName);
|
||||
if (suggestedVersion.isEmpty) {
|
||||
suggestedVersion = FlutterI18n.translate(
|
||||
context,
|
||||
'appSelectorCard.anyVersion',
|
||||
'appSelectorCard.allVersions',
|
||||
);
|
||||
} else {
|
||||
recommendedVersion = 'v$recommendedVersion';
|
||||
suggestedVersion = 'v$suggestedVersion';
|
||||
}
|
||||
return '${FlutterI18n.translate(
|
||||
context,
|
||||
'appSelectorCard.currentVersion',
|
||||
)}: v${selectedApp!.version}\n${FlutterI18n.translate(
|
||||
context,
|
||||
'appSelectorCard.recommendedVersion',
|
||||
)}: $recommendedVersion';
|
||||
'appSelectorCard.suggestedVersion',
|
||||
)}: $suggestedVersion';
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
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,8 +1,11 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/material.dart' hide SearchBar;
|
||||
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/widgets/patchesSelectorView/patch_item.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/search_bar.dart';
|
||||
import 'package:revanced_manager/utils/check_for_supported_patch.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
|
||||
class PatchesSelectorView extends StatefulWidget {
|
||||
@@ -14,22 +17,40 @@ class PatchesSelectorView extends StatefulWidget {
|
||||
|
||||
class _PatchesSelectorViewState extends State<PatchesSelectorView> {
|
||||
String _query = '';
|
||||
final _managerAPI = locator<ManagerAPI>();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
if (!_managerAPI.isPatchesChangeEnabled() &&
|
||||
_managerAPI.showPatchesChangeWarning()) {
|
||||
_managerAPI.showPatchesChangeWarningDialog(context);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ViewModelBuilder<PatchesSelectorViewModel>.reactive(
|
||||
onModelReady: (model) => model.initialize(),
|
||||
onViewModelReady: (model) => model.initialize(),
|
||||
viewModelBuilder: () => PatchesSelectorViewModel(),
|
||||
builder: (context, model, child) => Scaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
floatingActionButton: Visibility(
|
||||
visible: model.patches.isNotEmpty,
|
||||
child: FloatingActionButton.extended(
|
||||
label: I18nText('patchesSelectorView.doneButton'),
|
||||
label: Row(
|
||||
children: <Widget>[
|
||||
I18nText('patchesSelectorView.doneButton'),
|
||||
Text(' (${model.selectedPatches.length})'),
|
||||
],
|
||||
),
|
||||
icon: const Icon(Icons.check),
|
||||
onPressed: () {
|
||||
model.selectPatches();
|
||||
Navigator.of(context).pop();
|
||||
if (!model.areRequiredOptionsNull(context)) {
|
||||
model.selectPatches();
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -38,32 +59,62 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
|
||||
SliverAppBar(
|
||||
pinned: true,
|
||||
floating: true,
|
||||
snap: false,
|
||||
title: I18nText(
|
||||
'patchesSelectorView.viewTitle',
|
||||
child: Text(
|
||||
'',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).textTheme.headline6!.color,
|
||||
),
|
||||
),
|
||||
),
|
||||
titleTextStyle: TextStyle(
|
||||
fontSize: 22.0,
|
||||
color: Theme.of(context).textTheme.titleLarge!.color,
|
||||
),
|
||||
leading: IconButton(
|
||||
icon: Icon(
|
||||
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(
|
||||
preferredSize: const Size.fromHeight(64.0),
|
||||
preferredSize: const Size.fromHeight(66.0),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 8.0,
|
||||
horizontal: 12.0,
|
||||
),
|
||||
child: SearchBar(
|
||||
showSelectIcon: true,
|
||||
hintText: FlutterI18n.translate(
|
||||
context,
|
||||
'patchesSelectorView.searchBarHint',
|
||||
@@ -73,12 +124,6 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
|
||||
_query = searchQuery;
|
||||
});
|
||||
},
|
||||
onSelectAll: (value) {
|
||||
if (value) {
|
||||
model.selectAllPatcherWarning(context);
|
||||
}
|
||||
model.selectAllPatches(value);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -98,106 +143,234 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
|
||||
),
|
||||
)
|
||||
: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0)
|
||||
.copyWith(bottom: 80),
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 12.0).copyWith(
|
||||
bottom: MediaQuery.viewPaddingOf(context).bottom + 8.0,
|
||||
),
|
||||
child: Column(
|
||||
children: model
|
||||
.getQueriedPatches(_query)
|
||||
.map(
|
||||
(patch) => PatchItem(
|
||||
name: patch.name,
|
||||
simpleName: patch.getSimpleName(),
|
||||
version: patch.version,
|
||||
description: patch.description,
|
||||
packageVersion: model.getAppVersion(),
|
||||
supportedPackageVersions:
|
||||
model.getSupportedVersions(patch),
|
||||
isUnsupported: !model.isPatchSupported(patch),
|
||||
isSelected: model.isSelected(patch),
|
||||
onChanged: (value) =>
|
||||
model.selectPatch(patch, value),
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
ActionChip(
|
||||
label: I18nText('patchesSelectorView.default'),
|
||||
tooltip: FlutterI18n.translate(
|
||||
context,
|
||||
'patchesSelectorView.defaultTooltip',
|
||||
),
|
||||
onPressed: () {
|
||||
if (_managerAPI.isPatchesChangeEnabled()) {
|
||||
model.selectDefaultPatches();
|
||||
} else {
|
||||
model.showPatchesChangeDialog(context);
|
||||
}
|
||||
},
|
||||
),
|
||||
/* TODO: Enable this and make use of new Patch Options implementation
|
||||
patch.hasOptions ? ExpandablePanel(
|
||||
controller: expController,
|
||||
theme: const ExpandableThemeData(
|
||||
hasIcon: false,
|
||||
tapBodyToExpand: true,
|
||||
tapBodyToCollapse: true,
|
||||
tapHeaderToExpand: true,
|
||||
const SizedBox(width: 8),
|
||||
ActionChip(
|
||||
label: I18nText('patchesSelectorView.none'),
|
||||
tooltip: FlutterI18n.translate(
|
||||
context,
|
||||
'patchesSelectorView.noneTooltip',
|
||||
),
|
||||
onPressed: () {
|
||||
if (_managerAPI.isPatchesChangeEnabled()) {
|
||||
model.clearPatches();
|
||||
} else {
|
||||
model.showPatchesChangeDialog(context);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
if (model.newPatchExists())
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 10.0,
|
||||
),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 10.0,
|
||||
bottom: 10.0,
|
||||
left: 5.0,
|
||||
),
|
||||
header: Column(
|
||||
children: <Widget>[
|
||||
GestureDetector(
|
||||
onLongPress: () =>
|
||||
expController.toggle(),
|
||||
child: PatchItem(
|
||||
name: patch.name,
|
||||
simpleName: patch.getSimpleName(),
|
||||
description: patch.description,
|
||||
version: patch.version,
|
||||
packageVersion:
|
||||
model.getAppVersion(),
|
||||
supportedPackageVersions: model
|
||||
.getSupportedVersions(patch),
|
||||
isUnsupported: !model
|
||||
.isPatchSupported(patch),
|
||||
isSelected:
|
||||
model.isSelected(patch),
|
||||
onChanged: (value) => model
|
||||
.selectPatch(patch, value),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: 8.0,
|
||||
),
|
||||
child: Text(
|
||||
'Long press for additional options.',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
expanded: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 10.0,
|
||||
horizontal: 10,
|
||||
),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 8,
|
||||
horizontal: 8,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
child: I18nText(
|
||||
'patchesSelectorView.newPatches',
|
||||
child: Text(
|
||||
'',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.tertiary
|
||||
.withOpacity(0.1),
|
||||
borderRadius:
|
||||
BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'Patch options',
|
||||
style: GoogleFonts.inter(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const OptionsTextField(
|
||||
hint: 'App name'),
|
||||
const OptionsFilePicker(
|
||||
optionName: 'Choose a logo',
|
||||
),
|
||||
],
|
||||
.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
collapsed: Container(),
|
||||
) */
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
...model.getQueriedPatches(_query).map((patch) {
|
||||
if (model.isPatchNew(patch)) {
|
||||
return PatchItem(
|
||||
name: patch.name,
|
||||
simpleName: patch.getSimpleName(),
|
||||
description: patch.description ?? '',
|
||||
packageVersion:
|
||||
model.getAppInfo().version,
|
||||
supportedPackageVersions:
|
||||
model.getSupportedVersions(patch),
|
||||
isUnsupported: !isPatchSupported(patch),
|
||||
isChangeEnabled:
|
||||
_managerAPI.isPatchesChangeEnabled(),
|
||||
hasUnsupportedPatchOption:
|
||||
hasUnsupportedRequiredOption(
|
||||
patch.options,
|
||||
patch,
|
||||
),
|
||||
options: patch.options,
|
||||
isSelected: model.isSelected(patch),
|
||||
navigateToOptions: (options) =>
|
||||
model.navigateToPatchOptions(
|
||||
options,
|
||||
patch,
|
||||
),
|
||||
onChanged: (value) => model.selectPatch(
|
||||
patch,
|
||||
value,
|
||||
context,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return Container();
|
||||
}
|
||||
}),
|
||||
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(
|
||||
'patchesSelectorView.patches',
|
||||
child: Text(
|
||||
'',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
...model.getQueriedPatches(_query).map(
|
||||
(patch) {
|
||||
if (patch.compatiblePackages.isNotEmpty) {
|
||||
return PatchItem(
|
||||
name: patch.name,
|
||||
simpleName: patch.getSimpleName(),
|
||||
description: patch.description ?? '',
|
||||
packageVersion: model.getAppInfo().version,
|
||||
supportedPackageVersions:
|
||||
model.getSupportedVersions(patch),
|
||||
isUnsupported: !isPatchSupported(patch),
|
||||
isChangeEnabled:
|
||||
_managerAPI.isPatchesChangeEnabled(),
|
||||
hasUnsupportedPatchOption:
|
||||
hasUnsupportedRequiredOption(
|
||||
patch.options,
|
||||
patch,
|
||||
),
|
||||
options: patch.options,
|
||||
isSelected: model.isSelected(patch),
|
||||
navigateToOptions: (options) =>
|
||||
model.navigateToPatchOptions(
|
||||
options,
|
||||
patch,
|
||||
),
|
||||
onChanged: (value) => model.selectPatch(
|
||||
patch,
|
||||
value,
|
||||
context,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return Container();
|
||||
}
|
||||
},
|
||||
),
|
||||
if (_managerAPI.areUniversalPatchesEnabled())
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
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(
|
||||
'patchesSelectorView.universalPatches',
|
||||
child: Text(
|
||||
'',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
...model.getQueriedPatches(_query).map((patch) {
|
||||
if (patch.compatiblePackages.isEmpty) {
|
||||
return PatchItem(
|
||||
name: patch.name,
|
||||
simpleName: patch.getSimpleName(),
|
||||
description: patch.description ?? '',
|
||||
packageVersion:
|
||||
model.getAppInfo().version,
|
||||
supportedPackageVersions:
|
||||
model.getSupportedVersions(patch),
|
||||
isUnsupported: !isPatchSupported(patch),
|
||||
isChangeEnabled:
|
||||
_managerAPI.isPatchesChangeEnabled(),
|
||||
hasUnsupportedPatchOption:
|
||||
hasUnsupportedRequiredOption(
|
||||
patch.options,
|
||||
patch,
|
||||
),
|
||||
options: patch.options,
|
||||
isSelected: model.isSelected(patch),
|
||||
navigateToOptions: (options) =>
|
||||
model.navigateToPatchOptions(
|
||||
options,
|
||||
patch,
|
||||
),
|
||||
onChanged: (value) => model.selectPatch(
|
||||
patch,
|
||||
value,
|
||||
context,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return Container();
|
||||
}
|
||||
}),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 70.0),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,25 +1,52 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_i18n/widgets/I18nText.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/patched_application.dart';
|
||||
import 'package:revanced_manager/services/manager_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/widgets/shared/custom_material_button.dart';
|
||||
import 'package:revanced_manager/utils/check_for_supported_patch.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stacked_services/stacked_services.dart';
|
||||
|
||||
class PatchesSelectorViewModel extends BaseViewModel {
|
||||
final PatcherAPI _patcherAPI = locator<PatcherAPI>();
|
||||
final ManagerAPI _managerAPI = locator<ManagerAPI>();
|
||||
final NavigationService _navigationService = locator<NavigationService>();
|
||||
final List<Patch> patches = [];
|
||||
final List<Patch> currentSelection = [];
|
||||
final List<Patch> selectedPatches =
|
||||
locator<PatcherViewModel>().selectedPatches;
|
||||
PatchedApplication? selectedApp = locator<PatcherViewModel>().selectedApp;
|
||||
String? patchesVersion = '';
|
||||
bool isDefaultPatchesRepo() {
|
||||
return _managerAPI.getPatchesRepo() == 'revanced/revanced-patches';
|
||||
}
|
||||
|
||||
Future<void> initialize() async {
|
||||
patches.addAll(await _patcherAPI.getFilteredPatches(
|
||||
locator<PatcherViewModel>().selectedApp!.packageName,
|
||||
));
|
||||
patches.sort((a, b) => a.name.compareTo(b.name));
|
||||
getPatchesVersion().whenComplete(() => notifyListeners());
|
||||
patches.addAll(
|
||||
_patcherAPI.getFilteredPatches(
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -29,63 +56,198 @@ class PatchesSelectorViewModel extends BaseViewModel {
|
||||
);
|
||||
}
|
||||
|
||||
void selectPatch(Patch patch, bool isSelected) {
|
||||
if (isSelected && !selectedPatches.contains(patch)) {
|
||||
selectedPatches.add(patch);
|
||||
} else {
|
||||
selectedPatches.remove(patch);
|
||||
}
|
||||
notifyListeners();
|
||||
void navigateToPatchOptions(List<Option> setOptions, Patch patch) {
|
||||
_managerAPI.options = setOptions;
|
||||
_managerAPI.selectedPatch = patch;
|
||||
_managerAPI.modifiedOptions.clear();
|
||||
_navigationService.navigateToPatchOptionsView();
|
||||
}
|
||||
|
||||
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(
|
||||
barrierDismissible: false,
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: I18nText('patchesSelectorView.selectAllPatchesWarningTitle'),
|
||||
title: I18nText('notice'),
|
||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
content: I18nText('patchesSelectorView.selectAllPatchesWarningContent'),
|
||||
content: I18nText(
|
||||
'patchesSelectorView.setRequiredOption',
|
||||
translationParams: {
|
||||
'patches': patches.map((patch) => '• $patch').join('\n'),
|
||||
},
|
||||
),
|
||||
actions: <Widget>[
|
||||
CustomMaterialButton(
|
||||
label: I18nText('okButton'),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
)
|
||||
onPressed: () => {
|
||||
Navigator.of(context).pop(),
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void selectAllPatches(bool isSelected) {
|
||||
selectedPatches.clear();
|
||||
if (isSelected) {
|
||||
selectedPatches.addAll(patches);
|
||||
void selectPatch(Patch patch, bool isSelected, BuildContext context) {
|
||||
if (_managerAPI.isPatchesChangeEnabled()) {
|
||||
if (isSelected && !selectedPatches.contains(patch)) {
|
||||
selectedPatches.add(patch);
|
||||
} else {
|
||||
selectedPatches.remove(patch);
|
||||
}
|
||||
notifyListeners();
|
||||
} else {
|
||||
showPatchesChangeDialog(context);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> showPatchesChangeDialog(BuildContext context) async {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
title: I18nText('warning'),
|
||||
content: I18nText(
|
||||
'patchItem.patchesChangeWarningDialogText',
|
||||
child: const Text(
|
||||
'',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
CustomMaterialButton(
|
||||
isFilled: false,
|
||||
label: I18nText('okButton'),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
CustomMaterialButton(
|
||||
label: I18nText('patchItem.patchesChangeWarningDialogButton'),
|
||||
onPressed: () {
|
||||
Navigator.of(context)
|
||||
..pop()
|
||||
..pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
void selectPatches() {
|
||||
locator<PatcherViewModel>().selectedPatches = selectedPatches;
|
||||
saveSelectedPatches();
|
||||
if (_managerAPI.ctx != null) {
|
||||
Navigator.pop(_managerAPI.ctx!);
|
||||
_managerAPI.ctx = null;
|
||||
}
|
||||
locator<PatcherViewModel>().notifyListeners();
|
||||
}
|
||||
|
||||
List<Patch> getQueriedPatches(String query) {
|
||||
return patches
|
||||
.where((patch) =>
|
||||
query.isEmpty ||
|
||||
query.length < 2 ||
|
||||
patch.name.toLowerCase().contains(
|
||||
query.toLowerCase(),
|
||||
))
|
||||
.toList();
|
||||
void resetSelection() {
|
||||
selectedPatches.clear();
|
||||
selectedPatches.addAll(currentSelection);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
String getAppVersion() {
|
||||
return locator<PatcherViewModel>().selectedApp!.version;
|
||||
Future<void> getPatchesVersion() async {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
PatchedApplication getAppInfo() {
|
||||
return locator<PatcherViewModel>().selectedApp!;
|
||||
}
|
||||
|
||||
bool isPatchNew(Patch patch) {
|
||||
final List<Patch> savedPatches =
|
||||
_managerAPI.getSavedPatches(selectedApp!.packageName);
|
||||
if (savedPatches.isEmpty) {
|
||||
return false;
|
||||
} else {
|
||||
return !savedPatches
|
||||
.any((p) => p.getSimpleName() == patch.getSimpleName());
|
||||
}
|
||||
}
|
||||
|
||||
bool newPatchExists() {
|
||||
return patches.any(
|
||||
(patch) => isPatchNew(patch),
|
||||
);
|
||||
}
|
||||
|
||||
List<String> getSupportedVersions(Patch patch) {
|
||||
PatchedApplication app = locator<PatcherViewModel>().selectedApp!;
|
||||
Package? package = patch.compatiblePackages.firstWhereOrNull(
|
||||
final PatchedApplication app = locator<PatcherViewModel>().selectedApp!;
|
||||
final Package? package = patch.compatiblePackages.firstWhereOrNull(
|
||||
(pack) => pack.name == app.packageName,
|
||||
);
|
||||
if (package != null) {
|
||||
@@ -95,10 +257,42 @@ class PatchesSelectorViewModel extends BaseViewModel {
|
||||
}
|
||||
}
|
||||
|
||||
bool isPatchSupported(Patch patch) {
|
||||
PatchedApplication app = locator<PatcherViewModel>().selectedApp!;
|
||||
return patch.compatiblePackages.any((pack) =>
|
||||
pack.name == app.packageName &&
|
||||
(pack.versions.isEmpty || pack.versions.contains(app.version)));
|
||||
void onMenuSelection(value, BuildContext context) {
|
||||
switch (value) {
|
||||
case 0:
|
||||
loadSelectedPatches(context);
|
||||
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,121 @@
|
||||
// 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/services/toast.dart';
|
||||
import 'package:revanced_manager/ui/widgets/settingsView/custom_text_field.dart';
|
||||
import 'package:revanced_manager/ui/widgets/settingsView/settings_tile_dialog.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
|
||||
class SManageApiUrl extends BaseViewModel {
|
||||
final ManagerAPI _managerAPI = locator<ManagerAPI>();
|
||||
final Toast _toast = locator<Toast>();
|
||||
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
CustomTextField(
|
||||
leadingIcon: Icon(
|
||||
Icons.api_outlined,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
inputController: _apiUrlController,
|
||||
label: I18nText('settingsView.selectApiURL'),
|
||||
hint: apiUrl,
|
||||
onChanged: (value) => notifyListeners(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: <Widget>[
|
||||
CustomMaterialButton(
|
||||
isFilled: false,
|
||||
label: I18nText('cancelButton'),
|
||||
onPressed: () {
|
||||
_apiUrlController.clear();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
CustomMaterialButton(
|
||||
label: I18nText('okButton'),
|
||||
onPressed: () {
|
||||
String apiUrl = _apiUrlController.text;
|
||||
if (!apiUrl.startsWith('https')) {
|
||||
apiUrl = 'https://$apiUrl';
|
||||
}
|
||||
_managerAPI.setApiUrl(apiUrl);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> showApiUrlResetDialog(BuildContext context) async {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: I18nText('settingsView.sourcesResetDialogTitle'),
|
||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
content: I18nText('settingsView.apiURLResetDialogText'),
|
||||
actions: <Widget>[
|
||||
CustomMaterialButton(
|
||||
isFilled: false,
|
||||
label: I18nText('noButton'),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
CustomMaterialButton(
|
||||
label: I18nText('yesButton'),
|
||||
onPressed: () {
|
||||
_managerAPI.setApiUrl('');
|
||||
_toast.showBottom('settingsView.restartAppForChanges');
|
||||
Navigator.of(context)
|
||||
..pop()
|
||||
..pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
// 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/custom_text_field.dart';
|
||||
import 'package:revanced_manager/ui/widgets/settingsView/settings_tile_dialog.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
|
||||
class SManageKeystorePassword extends BaseViewModel {
|
||||
final ManagerAPI _managerAPI = locator<ManagerAPI>();
|
||||
|
||||
final TextEditingController _keystorePasswordController =
|
||||
TextEditingController();
|
||||
|
||||
Future<void> showKeystoreDialog(BuildContext context) async {
|
||||
final String keystorePasswordText = _managerAPI.getKeystorePassword();
|
||||
_keystorePasswordController.text = keystorePasswordText;
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Row(
|
||||
children: <Widget>[
|
||||
I18nText('settingsView.selectKeystorePassword'),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.manage_history_outlined),
|
||||
onPressed: () => _keystorePasswordController.text =
|
||||
_managerAPI.defaultKeystorePassword,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
CustomTextField(
|
||||
inputController: _keystorePasswordController,
|
||||
label: I18nText('settingsView.selectKeystorePassword'),
|
||||
hint: '',
|
||||
onChanged: (value) => notifyListeners(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: <Widget>[
|
||||
CustomMaterialButton(
|
||||
isFilled: false,
|
||||
label: I18nText('cancelButton'),
|
||||
onPressed: () {
|
||||
_keystorePasswordController.clear();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
CustomMaterialButton(
|
||||
label: I18nText('okButton'),
|
||||
onPressed: () {
|
||||
final String passwd = _keystorePasswordController.text;
|
||||
_managerAPI.setKeystorePassword(passwd);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final sManageKeystorePassword = SManageKeystorePassword();
|
||||
|
||||
class SManageKeystorePasswordUI extends StatelessWidget {
|
||||
const SManageKeystorePasswordUI({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SettingsTileDialog(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
title: 'settingsView.selectKeystorePassword',
|
||||
subtitle: 'settingsView.selectKeystorePasswordHint',
|
||||
onTap: () => sManageKeystorePassword.showKeystoreDialog(context),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
// 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/services/toast.dart';
|
||||
import 'package:revanced_manager/ui/widgets/settingsView/custom_text_field.dart';
|
||||
import 'package:revanced_manager/ui/widgets/settingsView/settings_tile_dialog.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
|
||||
class SManageSources extends BaseViewModel {
|
||||
final ManagerAPI _managerAPI = locator<ManagerAPI>();
|
||||
final Toast _toast = locator<Toast>();
|
||||
|
||||
final TextEditingController _hostSourceController = TextEditingController();
|
||||
final TextEditingController _orgPatSourceController = TextEditingController();
|
||||
final TextEditingController _patSourceController = TextEditingController();
|
||||
final TextEditingController _orgIntSourceController = TextEditingController();
|
||||
final TextEditingController _intSourceController = TextEditingController();
|
||||
|
||||
Future<void> showSourcesDialog(BuildContext context) async {
|
||||
final String hostRepository = _managerAPI.getRepoUrl();
|
||||
final String patchesRepo = _managerAPI.getPatchesRepo();
|
||||
final String integrationsRepo = _managerAPI.getIntegrationsRepo();
|
||||
_hostSourceController.text = hostRepository;
|
||||
_orgPatSourceController.text = patchesRepo.split('/')[0];
|
||||
_patSourceController.text = patchesRepo.split('/')[1];
|
||||
_orgIntSourceController.text = integrationsRepo.split('/')[0];
|
||||
_intSourceController.text = integrationsRepo.split('/')[1];
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Row(
|
||||
children: <Widget>[
|
||||
I18nText('settingsView.sourcesLabel'),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.manage_history_outlined),
|
||||
onPressed: () => showResetConfirmationDialog(context),
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
CustomTextField(
|
||||
leadingIcon: const Icon(
|
||||
Icons.extension_outlined,
|
||||
color: Colors.transparent,
|
||||
),
|
||||
inputController: _hostSourceController,
|
||||
label: I18nText('settingsView.hostRepositoryLabel'),
|
||||
hint: hostRepository,
|
||||
onChanged: (value) => notifyListeners(),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
CustomTextField(
|
||||
leadingIcon: Icon(
|
||||
Icons.extension_outlined,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
inputController: _orgPatSourceController,
|
||||
label: I18nText('settingsView.orgPatchesLabel'),
|
||||
hint: patchesRepo.split('/')[0],
|
||||
onChanged: (value) => notifyListeners(),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
CustomTextField(
|
||||
leadingIcon: const Icon(
|
||||
Icons.extension_outlined,
|
||||
color: Colors.transparent,
|
||||
),
|
||||
inputController: _patSourceController,
|
||||
label: I18nText('settingsView.sourcesPatchesLabel'),
|
||||
hint: patchesRepo.split('/')[1],
|
||||
onChanged: (value) => notifyListeners(),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
CustomTextField(
|
||||
leadingIcon: Icon(
|
||||
Icons.merge_outlined,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
inputController: _orgIntSourceController,
|
||||
label: I18nText('settingsView.orgIntegrationsLabel'),
|
||||
hint: integrationsRepo.split('/')[0],
|
||||
onChanged: (value) => notifyListeners(),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
CustomTextField(
|
||||
leadingIcon: const Icon(
|
||||
Icons.merge_outlined,
|
||||
color: Colors.transparent,
|
||||
),
|
||||
inputController: _intSourceController,
|
||||
label: I18nText('settingsView.sourcesIntegrationsLabel'),
|
||||
hint: integrationsRepo.split('/')[1],
|
||||
onChanged: (value) => notifyListeners(),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
I18nText('settingsView.sourcesUpdateNote'),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: <Widget>[
|
||||
CustomMaterialButton(
|
||||
isFilled: false,
|
||||
label: I18nText('cancelButton'),
|
||||
onPressed: () {
|
||||
_orgPatSourceController.clear();
|
||||
_patSourceController.clear();
|
||||
_orgIntSourceController.clear();
|
||||
_intSourceController.clear();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
CustomMaterialButton(
|
||||
label: I18nText('okButton'),
|
||||
onPressed: () {
|
||||
_managerAPI.setRepoUrl(_hostSourceController.text.trim());
|
||||
_managerAPI.setPatchesRepo(
|
||||
'${_orgPatSourceController.text.trim()}/${_patSourceController.text.trim()}',
|
||||
);
|
||||
_managerAPI.setIntegrationsRepo(
|
||||
'${_orgIntSourceController.text.trim()}/${_intSourceController.text.trim()}',
|
||||
);
|
||||
_managerAPI.setCurrentPatchesVersion('0.0.0');
|
||||
_managerAPI.setCurrentIntegrationsVersion('0.0.0');
|
||||
_toast.showBottom('settingsView.restartAppForChanges');
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> showResetConfirmationDialog(BuildContext context) async {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: I18nText('settingsView.sourcesResetDialogTitle'),
|
||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
content: I18nText('settingsView.sourcesResetDialogText'),
|
||||
actions: <Widget>[
|
||||
CustomMaterialButton(
|
||||
isFilled: false,
|
||||
label: I18nText('noButton'),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
CustomMaterialButton(
|
||||
label: I18nText('yesButton'),
|
||||
onPressed: () {
|
||||
_managerAPI.setRepoUrl('');
|
||||
_managerAPI.setPatchesRepo('');
|
||||
_managerAPI.setIntegrationsRepo('');
|
||||
_managerAPI.setCurrentPatchesVersion('0.0.0');
|
||||
_managerAPI.setCurrentIntegrationsVersion('0.0.0');
|
||||
_toast.showBottom('settingsView.restartAppForChanges');
|
||||
Navigator.of(context)
|
||||
..pop()
|
||||
..pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final sManageSources = SManageSources();
|
||||
|
||||
class SManageSourcesUI extends StatelessWidget {
|
||||
const SManageSourcesUI({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SettingsTileDialog(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
title: 'settingsView.sourcesLabel',
|
||||
subtitle: 'settingsView.sourcesLabelHint',
|
||||
onTap: () => sManageSources.showSourcesDialog(context),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
// 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/main.dart';
|
||||
import 'package:revanced_manager/services/toast.dart';
|
||||
import 'package:revanced_manager/ui/views/navigation/navigation_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/widgets/settingsView/settings_tile_dialog.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
import 'package:timeago/timeago.dart' as timeago;
|
||||
|
||||
final _settingViewModel = SettingsViewModel();
|
||||
|
||||
class SUpdateLanguage extends BaseViewModel {
|
||||
final Toast _toast = locator<Toast>();
|
||||
late SharedPreferences _prefs;
|
||||
String selectedLanguage = 'English';
|
||||
String selectedLanguageLocale = prefs.getString('language') ?? 'en_US';
|
||||
List languages = [];
|
||||
|
||||
Future<void> initialize() async {
|
||||
_prefs = await SharedPreferences.getInstance();
|
||||
selectedLanguageLocale =
|
||||
_prefs.getString('language') ?? selectedLanguageLocale;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> updateLanguage(BuildContext context, String? value) async {
|
||||
if (value != null) {
|
||||
selectedLanguageLocale = value;
|
||||
_prefs = await SharedPreferences.getInstance();
|
||||
await _prefs.setString('language', value);
|
||||
await FlutterI18n.refresh(context, Locale(value));
|
||||
timeago.setLocaleMessages(value, timeago.EnMessages());
|
||||
locator<NavigationViewModel>().notifyListeners();
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> initLang() async {
|
||||
languages.sort((a, b) => a['name'].compareTo(b['name']));
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> showLanguagesDialog(BuildContext parentContext) {
|
||||
initLang();
|
||||
return showDialog(
|
||||
context: parentContext,
|
||||
builder: (context) => SimpleDialog(
|
||||
title: I18nText('settingsView.languageLabel'),
|
||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 500,
|
||||
child: ListView.builder(
|
||||
itemCount: languages.length,
|
||||
itemBuilder: (context, index) {
|
||||
return RadioListTile<String>(
|
||||
title: Text(languages[index]['name']),
|
||||
subtitle: Text(languages[index]['locale']),
|
||||
value: languages[index]['locale'],
|
||||
groupValue: selectedLanguageLocale,
|
||||
onChanged: (value) {
|
||||
selectedLanguage = languages[index]['name'];
|
||||
_toast.showBottom('settingsView.restartAppForChanges');
|
||||
updateLanguage(context, value);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SUpdateLanguageUI extends StatelessWidget {
|
||||
const SUpdateLanguageUI({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SettingsTileDialog(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
title: 'settingsView.languageLabel',
|
||||
subtitle: _settingViewModel.sUpdateLanguage.selectedLanguage,
|
||||
onTap: () =>
|
||||
_settingViewModel.sUpdateLanguage.showLanguagesDialog(context),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
// ignore_for_file: use_build_context_synchronously
|
||||
|
||||
import 'package:dynamic_themes/dynamic_themes.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_i18n/widgets/I18nText.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_section.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
|
||||
|
||||
class SUpdateThemeUI extends StatefulWidget {
|
||||
const SUpdateThemeUI({super.key});
|
||||
|
||||
@override
|
||||
State<SUpdateThemeUI> createState() => _SUpdateThemeUIState();
|
||||
}
|
||||
|
||||
class _SUpdateThemeUIState extends State<SUpdateThemeUI> {
|
||||
final ManagerAPI managerAPI = locator<ManagerAPI>();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SettingsSection(
|
||||
title: 'settingsView.appearanceSectionTitle',
|
||||
children: <Widget>[
|
||||
ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
title: I18nText(
|
||||
'settingsView.themeModeLabel',
|
||||
child: const Text(
|
||||
'',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
trailing: CustomMaterialButton(
|
||||
label: getThemeModeName(),
|
||||
onPressed: () => {showThemeDialog(context)},
|
||||
),
|
||||
onTap: () => {showThemeDialog(context)},
|
||||
),
|
||||
if (managerAPI.isDynamicThemeAvailable)
|
||||
SwitchListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
title: I18nText(
|
||||
'settingsView.dynamicThemeLabel',
|
||||
child: const Text(
|
||||
'',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
subtitle: I18nText('settingsView.dynamicThemeHint'),
|
||||
value: getDynamicThemeStatus(),
|
||||
onChanged: (value) => {
|
||||
setUseDynamicTheme(
|
||||
context,
|
||||
value,
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
bool getDynamicThemeStatus() {
|
||||
return managerAPI.getUseDynamicTheme();
|
||||
}
|
||||
|
||||
Future<void> setUseDynamicTheme(BuildContext context, bool value) async {
|
||||
await managerAPI.setUseDynamicTheme(value);
|
||||
final int currentTheme = (DynamicTheme.of(context)!.themeId ~/ 2) * 2;
|
||||
await DynamicTheme.of(context)!.setTheme(currentTheme + (value ? 1 : 0));
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
int getThemeMode() {
|
||||
return managerAPI.getThemeMode();
|
||||
}
|
||||
|
||||
Future<void> setThemeMode(BuildContext context, int value) async {
|
||||
await managerAPI.setThemeMode(value);
|
||||
final bool isDynamicTheme = DynamicTheme.of(context)!.themeId.isEven;
|
||||
await DynamicTheme.of(context)!
|
||||
.setTheme(value * 2 + (isDynamicTheme ? 0 : 1));
|
||||
final bool isLight = value != 2 &&
|
||||
(value == 1 ||
|
||||
DynamicTheme.of(context)!.theme.brightness == Brightness.light);
|
||||
SystemChrome.setSystemUIOverlayStyle(
|
||||
SystemUiOverlayStyle(
|
||||
systemNavigationBarIconBrightness:
|
||||
isLight ? Brightness.dark : Brightness.light,
|
||||
),
|
||||
);
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
I18nText getThemeModeName() {
|
||||
switch (getThemeMode()) {
|
||||
case 0:
|
||||
return I18nText('settingsView.systemThemeLabel');
|
||||
case 1:
|
||||
return I18nText('settingsView.lightThemeLabel');
|
||||
case 2:
|
||||
return I18nText('settingsView.darkThemeLabel');
|
||||
default:
|
||||
return I18nText('settingsView.systemThemeLabel');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> showThemeDialog(BuildContext context) async {
|
||||
final ValueNotifier<int> newTheme = ValueNotifier(getThemeMode());
|
||||
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: I18nText('settingsView.themeModeLabel'),
|
||||
icon: const Icon(Icons.palette),
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 16),
|
||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
content: SingleChildScrollView(
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: newTheme,
|
||||
builder: (context, value, child) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
RadioListTile(
|
||||
title: I18nText('settingsView.systemThemeLabel'),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
value: 0,
|
||||
groupValue: value,
|
||||
onChanged: (value) {
|
||||
newTheme.value = value!;
|
||||
},
|
||||
),
|
||||
RadioListTile(
|
||||
title: I18nText('settingsView.lightThemeLabel'),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
value: 1,
|
||||
groupValue: value,
|
||||
onChanged: (value) {
|
||||
newTheme.value = value!;
|
||||
},
|
||||
),
|
||||
RadioListTile(
|
||||
title: I18nText('settingsView.darkThemeLabel'),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
value: 2,
|
||||
groupValue: value,
|
||||
onChanged: (value) {
|
||||
newTheme.value = value!;
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
actions: <Widget>[
|
||||
CustomMaterialButton(
|
||||
isFilled: false,
|
||||
label: I18nText('cancelButton'),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
CustomMaterialButton(
|
||||
label: I18nText('okButton'),
|
||||
onPressed: () {
|
||||
setThemeMode(context, newTheme.value);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,14 @@
|
||||
// ignore_for_file: prefer_const_constructors
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_i18n/flutter_i18n.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:revanced_manager/ui/views/settings/settingsFragment/settings_update_theme.dart';
|
||||
import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/widgets/settingsView/about_widget.dart';
|
||||
import 'package:revanced_manager/ui/widgets/settingsView/custom_switch_tile.dart';
|
||||
import 'package:revanced_manager/ui/widgets/settingsView/settings_tile_dialog.dart';
|
||||
import 'package:revanced_manager/ui/widgets/settingsView/settings_section.dart';
|
||||
import 'package:revanced_manager/ui/widgets/settingsView/social_media_widget.dart';
|
||||
import 'package:revanced_manager/ui/widgets/settingsView/settings_advanced_section.dart';
|
||||
import 'package:revanced_manager/ui/widgets/settingsView/settings_debug_section.dart';
|
||||
import 'package:revanced_manager/ui/widgets/settingsView/settings_export_section.dart';
|
||||
import 'package:revanced_manager/ui/widgets/settingsView/settings_team_section.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/custom_sliver_app_bar.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
|
||||
@@ -30,7 +32,7 @@ class SettingsView extends StatelessWidget {
|
||||
child: Text(
|
||||
'',
|
||||
style: GoogleFonts.inter(
|
||||
color: Theme.of(context).textTheme.headline6!.color,
|
||||
color: Theme.of(context).textTheme.titleLarge!.color,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -38,129 +40,22 @@ class SettingsView extends StatelessWidget {
|
||||
SliverList(
|
||||
delegate: SliverChildListDelegate.fixed(
|
||||
<Widget>[
|
||||
SettingsSection(
|
||||
title: 'settingsView.appearanceSectionTitle',
|
||||
children: <Widget>[
|
||||
CustomSwitchTile(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
title: I18nText(
|
||||
'settingsView.darkThemeLabel',
|
||||
child: const Text(
|
||||
'',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
subtitle: I18nText('settingsView.darkThemeHint'),
|
||||
value: model.getDarkThemeStatus(),
|
||||
onTap: (value) => model.setUseDarkTheme(
|
||||
context,
|
||||
value,
|
||||
),
|
||||
),
|
||||
FutureBuilder<int>(
|
||||
future: model.getSdkVersion(),
|
||||
builder: (context, snapshot) => Visibility(
|
||||
visible: snapshot.hasData &&
|
||||
snapshot.data! >= ANDROID_12_SDK_VERSION,
|
||||
child: CustomSwitchTile(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
title: I18nText(
|
||||
'settingsView.dynamicThemeLabel',
|
||||
child: const Text(
|
||||
'',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
subtitle: I18nText('settingsView.dynamicThemeHint'),
|
||||
value: model.getDynamicThemeStatus(),
|
||||
onTap: (value) => model.setUseDynamicTheme(
|
||||
context,
|
||||
value,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SettingsTileDialog(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
title: 'settingsView.languageLabel',
|
||||
subtitle: 'English',
|
||||
onTap: () => model.showLanguagesDialog(context),
|
||||
),
|
||||
_settingsDivider,
|
||||
SettingsSection(
|
||||
title: 'settingsView.teamSectionTitle',
|
||||
children: <Widget>[
|
||||
ListTile(
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
title: I18nText(
|
||||
'settingsView.contributorsLabel',
|
||||
child: const Text(
|
||||
'',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
subtitle: I18nText('settingsView.contributorsHint'),
|
||||
onTap: () => model.navigateToContributors(),
|
||||
),
|
||||
const SocialMediaWidget(
|
||||
padding: EdgeInsets.symmetric(horizontal: 20.0),
|
||||
),
|
||||
],
|
||||
),
|
||||
_settingsDivider,
|
||||
SettingsSection(
|
||||
title: 'settingsView.advancedSectionTitle',
|
||||
children: <Widget>[
|
||||
SettingsTileDialog(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
title: 'settingsView.apiURLLabel',
|
||||
subtitle: 'settingsView.apiURLHint',
|
||||
onTap: () => model.showApiUrlDialog(context),
|
||||
),
|
||||
SettingsTileDialog(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
title: 'settingsView.sourcesLabel',
|
||||
subtitle: 'settingsView.sourcesLabelHint',
|
||||
onTap: () => model.showSourcesDialog(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
_settingsDivider,
|
||||
SettingsSection(
|
||||
title: 'settingsView.infoSectionTitle',
|
||||
children: <Widget>[
|
||||
ListTile(
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
title: I18nText(
|
||||
'settingsView.logsLabel',
|
||||
child: const Text(
|
||||
'',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
subtitle: I18nText('settingsView.logsHint'),
|
||||
onTap: () => model.exportLogcatLogs(),
|
||||
),
|
||||
const AboutWidget(
|
||||
padding: EdgeInsets.symmetric(horizontal: 20.0),
|
||||
),
|
||||
ListView(
|
||||
padding: EdgeInsets.zero,
|
||||
shrinkWrap: true,
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
children: const [
|
||||
SUpdateThemeUI(),
|
||||
// _settingsDivider,
|
||||
// SUpdateLanguageUI(),
|
||||
_settingsDivider,
|
||||
SAdvancedSection(),
|
||||
_settingsDivider,
|
||||
SExportSection(),
|
||||
_settingsDivider,
|
||||
STeamSection(),
|
||||
_settingsDivider,
|
||||
SDebugSection(),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
@@ -1,335 +1,280 @@
|
||||
// ignore_for_file: use_build_context_synchronously
|
||||
import 'dart:io';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:dynamic_themes/dynamic_themes.dart';
|
||||
import 'package:cr_file_saver/file_saver.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_i18n/flutter_i18n.dart';
|
||||
import 'package:logcat/logcat.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:revanced_manager/app/app.locator.dart';
|
||||
import 'package:revanced_manager/app/app.router.dart';
|
||||
import 'package:revanced_manager/services/manager_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/patches_selector/patches_selector_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/views/settings/settingsFragment/settings_update_language.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
|
||||
import 'package:revanced_manager/ui/widgets/settingsView/custom_text_field.dart';
|
||||
import 'package:share_extend/share_extend.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
import 'package:stacked_services/stacked_services.dart';
|
||||
import 'package:timeago/timeago.dart';
|
||||
|
||||
// ignore: constant_identifier_names
|
||||
const int ANDROID_12_SDK_VERSION = 31;
|
||||
|
||||
class SettingsViewModel extends BaseViewModel {
|
||||
final NavigationService _navigationService = locator<NavigationService>();
|
||||
final ManagerAPI _managerAPI = locator<ManagerAPI>();
|
||||
final TextEditingController _orgPatSourceController = TextEditingController();
|
||||
final TextEditingController _patSourceController = TextEditingController();
|
||||
final TextEditingController _orgIntSourceController = TextEditingController();
|
||||
final TextEditingController _intSourceController = TextEditingController();
|
||||
final TextEditingController _apiUrlController = TextEditingController();
|
||||
final PatchesSelectorViewModel _patchesSelectorViewModel =
|
||||
PatchesSelectorViewModel();
|
||||
final PatcherViewModel _patcherViewModel = locator<PatcherViewModel>();
|
||||
final Toast _toast = locator<Toast>();
|
||||
|
||||
void setLanguage(String language) {
|
||||
notifyListeners();
|
||||
}
|
||||
final SUpdateLanguage sUpdateLanguage = SUpdateLanguage();
|
||||
|
||||
void navigateToContributors() {
|
||||
_navigationService.navigateTo(Routes.contributorsView);
|
||||
}
|
||||
|
||||
Future<void> updateLanguage(BuildContext context, String? value) async {
|
||||
if (value != null) {
|
||||
await FlutterI18n.refresh(context, Locale(value));
|
||||
setLocaleMessages(value, EnMessages());
|
||||
}
|
||||
bool isPatchesAutoUpdate() {
|
||||
return _managerAPI.isPatchesAutoUpdate();
|
||||
}
|
||||
|
||||
bool getDynamicThemeStatus() {
|
||||
return _managerAPI.getUseDynamicTheme();
|
||||
}
|
||||
|
||||
void setUseDynamicTheme(BuildContext context, bool value) async {
|
||||
await _managerAPI.setUseDynamicTheme(value);
|
||||
int currentTheme = DynamicTheme.of(context)!.themeId;
|
||||
if (currentTheme.isEven) {
|
||||
await DynamicTheme.of(context)!.setTheme(value ? 2 : 0);
|
||||
} else {
|
||||
await DynamicTheme.of(context)!.setTheme(value ? 3 : 1);
|
||||
}
|
||||
void setPatchesAutoUpdate(bool value) {
|
||||
_managerAPI.setPatchesAutoUpdate(value);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool getDarkThemeStatus() {
|
||||
return _managerAPI.getUseDarkTheme();
|
||||
bool isPatchesChangeEnabled() {
|
||||
return _managerAPI.isPatchesChangeEnabled();
|
||||
}
|
||||
|
||||
void setUseDarkTheme(BuildContext context, bool value) async {
|
||||
await _managerAPI.setUseDarkTheme(value);
|
||||
int currentTheme = DynamicTheme.of(context)!.themeId;
|
||||
if (currentTheme < 2) {
|
||||
await DynamicTheme.of(context)!.setTheme(value ? 1 : 0);
|
||||
Future<void> showPatchesChangeEnableDialog(
|
||||
bool value,
|
||||
BuildContext context,
|
||||
) async {
|
||||
if (value) {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
title: I18nText('warning'),
|
||||
content: I18nText(
|
||||
'settingsView.enablePatchesSelectionWarningText',
|
||||
child: const Text(
|
||||
'',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
CustomMaterialButton(
|
||||
isFilled: false,
|
||||
label: I18nText('yesButton'),
|
||||
onPressed: () {
|
||||
_managerAPI.setChangingToggleModified(true);
|
||||
_managerAPI.setPatchesChangeEnabled(true);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
CustomMaterialButton(
|
||||
label: I18nText('noButton'),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
await DynamicTheme.of(context)!.setTheme(value ? 3 : 2);
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
title: I18nText('warning'),
|
||||
content: I18nText(
|
||||
'settingsView.disablePatchesSelectionWarningText',
|
||||
child: const Text(
|
||||
'',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
CustomMaterialButton(
|
||||
isFilled: false,
|
||||
label: I18nText('noButton'),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
CustomMaterialButton(
|
||||
label: I18nText('yesButton'),
|
||||
onPressed: () {
|
||||
_managerAPI.setChangingToggleModified(true);
|
||||
_patchesSelectorViewModel.selectDefaultPatches();
|
||||
_managerAPI.setPatchesChangeEnabled(false);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
SystemChrome.setSystemUIOverlayStyle(
|
||||
SystemUiOverlayStyle(
|
||||
systemNavigationBarIconBrightness:
|
||||
value ? Brightness.light : Brightness.dark,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
bool areUniversalPatchesEnabled() {
|
||||
return _managerAPI.areUniversalPatchesEnabled();
|
||||
}
|
||||
|
||||
void showUniversalPatches(bool value) {
|
||||
_managerAPI.enableUniversalPatchesStatus(value);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> showLanguagesDialog(BuildContext context) {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => SimpleDialog(
|
||||
title: I18nText('settingsView.languageLabel'),
|
||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
children: <Widget>[
|
||||
RadioListTile<String>(
|
||||
title: I18nText('settingsView.englishOption'),
|
||||
value: 'en',
|
||||
groupValue: 'en',
|
||||
onChanged: (value) {
|
||||
updateLanguage(context, value);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
bool isVersionCompatibilityCheckEnabled() {
|
||||
return _managerAPI.isVersionCompatibilityCheckEnabled();
|
||||
}
|
||||
|
||||
Future<void> showSourcesDialog(BuildContext context) async {
|
||||
String patchesRepo = _managerAPI.getPatchesRepo();
|
||||
String integrationsRepo = _managerAPI.getIntegrationsRepo();
|
||||
_orgPatSourceController.text = patchesRepo.split('/')[0];
|
||||
_patSourceController.text = patchesRepo.split('/')[1];
|
||||
_orgIntSourceController.text = integrationsRepo.split('/')[0];
|
||||
_intSourceController.text = integrationsRepo.split('/')[1];
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Row(
|
||||
children: <Widget>[
|
||||
I18nText('settingsView.sourcesLabel'),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.manage_history_outlined),
|
||||
onPressed: () => showResetConfirmationDialog(context),
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
)
|
||||
],
|
||||
),
|
||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
CustomTextField(
|
||||
leadingIcon: Icon(
|
||||
Icons.extension_outlined,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
inputController: _orgPatSourceController,
|
||||
label: I18nText('settingsView.orgPatchesLabel'),
|
||||
hint: patchesRepo.split('/')[0],
|
||||
onChanged: (value) => notifyListeners(),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
CustomTextField(
|
||||
leadingIcon: const Icon(
|
||||
Icons.extension_outlined,
|
||||
color: Colors.transparent,
|
||||
),
|
||||
inputController: _patSourceController,
|
||||
label: I18nText('settingsView.sourcesPatchesLabel'),
|
||||
hint: patchesRepo.split('/')[1],
|
||||
onChanged: (value) => notifyListeners(),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
CustomTextField(
|
||||
leadingIcon: Icon(
|
||||
Icons.merge_outlined,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
inputController: _orgIntSourceController,
|
||||
label: I18nText('settingsView.orgIntegrationsLabel'),
|
||||
hint: integrationsRepo.split('/')[0],
|
||||
onChanged: (value) => notifyListeners(),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
CustomTextField(
|
||||
leadingIcon: const Icon(
|
||||
Icons.merge_outlined,
|
||||
color: Colors.transparent,
|
||||
),
|
||||
inputController: _intSourceController,
|
||||
label: I18nText('settingsView.sourcesIntegrationsLabel'),
|
||||
hint: integrationsRepo.split('/')[1],
|
||||
onChanged: (value) => notifyListeners(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: <Widget>[
|
||||
CustomMaterialButton(
|
||||
isFilled: false,
|
||||
label: I18nText('cancelButton'),
|
||||
onPressed: () {
|
||||
_orgPatSourceController.clear();
|
||||
_patSourceController.clear();
|
||||
_orgIntSourceController.clear();
|
||||
_intSourceController.clear();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
CustomMaterialButton(
|
||||
label: I18nText('okButton'),
|
||||
onPressed: () {
|
||||
_managerAPI.setPatchesRepo(
|
||||
'${_orgPatSourceController.text}/${_patSourceController.text}',
|
||||
);
|
||||
_managerAPI.setIntegrationsRepo(
|
||||
'${_orgIntSourceController.text}/${_intSourceController.text}',
|
||||
);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
void useVersionCompatibilityCheck(bool value) {
|
||||
_managerAPI.enableVersionCompatibilityCheckStatus(value);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> showApiUrlDialog(BuildContext context) async {
|
||||
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,
|
||||
)
|
||||
],
|
||||
),
|
||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
CustomTextField(
|
||||
leadingIcon: Icon(
|
||||
Icons.api_outlined,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
inputController: _apiUrlController,
|
||||
label: I18nText('settingsView.selectApiURL'),
|
||||
hint: apiUrl.split('/')[0],
|
||||
onChanged: (value) => notifyListeners(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: <Widget>[
|
||||
CustomMaterialButton(
|
||||
isFilled: false,
|
||||
label: I18nText('cancelButton'),
|
||||
onPressed: () {
|
||||
_apiUrlController.clear();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
CustomMaterialButton(
|
||||
label: I18nText('okButton'),
|
||||
onPressed: () {
|
||||
String apiUrl = _apiUrlController.text;
|
||||
if (!apiUrl.startsWith('https')) {
|
||||
apiUrl = 'https://$apiUrl';
|
||||
}
|
||||
_managerAPI.setApiUrl(apiUrl);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
void deleteKeystore() {
|
||||
_managerAPI.deleteKeystore();
|
||||
_toast.showBottom('settingsView.regeneratedKeystore');
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> showResetConfirmationDialog(BuildContext context) async {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: I18nText('settingsView.sourcesResetDialogTitle'),
|
||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
content: I18nText('settingsView.sourcesResetDialogText'),
|
||||
actions: <Widget>[
|
||||
CustomMaterialButton(
|
||||
isFilled: false,
|
||||
label: I18nText('noButton'),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
CustomMaterialButton(
|
||||
label: I18nText('yesButton'),
|
||||
onPressed: () {
|
||||
_managerAPI.setPatchesRepo('');
|
||||
_managerAPI.setIntegrationsRepo('');
|
||||
Navigator.of(context).pop();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
void deleteTempDir() {
|
||||
_managerAPI.deleteTempFolder();
|
||||
_toast.showBottom('settingsView.deletedTempDir');
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> showApiUrlResetDialog(BuildContext context) async {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: I18nText('settingsView.sourcesResetDialogTitle'),
|
||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
content: I18nText('settingsView.apiURLResetDialogText'),
|
||||
actions: <Widget>[
|
||||
CustomMaterialButton(
|
||||
isFilled: false,
|
||||
label: I18nText('noButton'),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
Future<void> exportPatches() async {
|
||||
try {
|
||||
final File outFile = File(_managerAPI.storedPatchesFile);
|
||||
if (outFile.existsSync()) {
|
||||
final String dateTime =
|
||||
DateTime.now().toString().replaceAll(' ', '_').split('.').first;
|
||||
await CRFileSaver.saveFileWithDialog(
|
||||
SaveFileDialogParams(
|
||||
sourceFilePath: outFile.path,
|
||||
destinationFileName: 'selected_patches_$dateTime.json',
|
||||
),
|
||||
CustomMaterialButton(
|
||||
label: I18nText('yesButton'),
|
||||
onPressed: () {
|
||||
_managerAPI.setApiUrl('');
|
||||
Navigator.of(context).pop();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
);
|
||||
_toast.showBottom('settingsView.exportedPatches');
|
||||
} else {
|
||||
_toast.showBottom('settingsView.noExportFileFound');
|
||||
}
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<int> getSdkVersion() async {
|
||||
AndroidDeviceInfo info = await DeviceInfoPlugin().androidInfo;
|
||||
return info.version.sdkInt ?? -1;
|
||||
Future<void> importPatches(BuildContext context) async {
|
||||
if (isPatchesChangeEnabled()) {
|
||||
try {
|
||||
final FilePickerResult? result = await FilePicker.platform.pickFiles(
|
||||
type: FileType.custom,
|
||||
allowedExtensions: ['json'],
|
||||
);
|
||||
if (result != null && result.files.single.path != null) {
|
||||
final File inFile = File(result.files.single.path!);
|
||||
inFile.copySync(_managerAPI.storedPatchesFile);
|
||||
inFile.delete();
|
||||
if (_patcherViewModel.selectedApp != null) {
|
||||
_patcherViewModel.loadLastSelectedPatches();
|
||||
}
|
||||
_toast.showBottom('settingsView.importedPatches');
|
||||
}
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
_toast.showBottom('settingsView.jsonSelectorErrorMessage');
|
||||
}
|
||||
} else {
|
||||
_managerAPI.showPatchesChangeWarningDialog(context);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> exportKeystore() async {
|
||||
try {
|
||||
final File outFile = File(_managerAPI.keystoreFile);
|
||||
if (outFile.existsSync()) {
|
||||
final String dateTime =
|
||||
DateTime.now().toString().replaceAll(' ', '_').split('.').first;
|
||||
await CRFileSaver.saveFileWithDialog(
|
||||
SaveFileDialogParams(
|
||||
sourceFilePath: outFile.path,
|
||||
destinationFileName: 'keystore_$dateTime.keystore',
|
||||
),
|
||||
);
|
||||
_toast.showBottom('settingsView.exportedKeystore');
|
||||
} else {
|
||||
_toast.showBottom('settingsView.noKeystoreExportFileFound');
|
||||
}
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> importKeystore() async {
|
||||
try {
|
||||
final FilePickerResult? result = await FilePicker.platform.pickFiles();
|
||||
if (result != null && result.files.single.path != null) {
|
||||
final File inFile = File(result.files.single.path!);
|
||||
inFile.copySync(_managerAPI.keystoreFile);
|
||||
|
||||
_toast.showBottom('settingsView.importedKeystore');
|
||||
}
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
_toast.showBottom('settingsView.keystoreSelectorErrorMessage');
|
||||
}
|
||||
}
|
||||
|
||||
void resetAllOptions() {
|
||||
_managerAPI.resetAllOptions();
|
||||
_toast.showBottom('settingsView.resetStoredOptions');
|
||||
}
|
||||
|
||||
void resetSelectedPatches() {
|
||||
_managerAPI.resetLastSelectedPatches();
|
||||
_toast.showBottom('settingsView.resetStoredPatches');
|
||||
}
|
||||
|
||||
Future<void> deleteLogs() async {
|
||||
final Directory appCacheDir = await getTemporaryDirectory();
|
||||
final Directory logsDir = Directory('${appCacheDir.path}/logs');
|
||||
if (logsDir.existsSync()) {
|
||||
logsDir.deleteSync(recursive: true);
|
||||
}
|
||||
_toast.showBottom('settingsView.deletedLogs');
|
||||
}
|
||||
|
||||
Future<void> exportLogcatLogs() async {
|
||||
Directory appCache = await getTemporaryDirectory();
|
||||
Directory logDir = Directory('${appCache.path}/logs');
|
||||
final Directory appCache = await getTemporaryDirectory();
|
||||
final Directory logDir = Directory('${appCache.path}/logs');
|
||||
logDir.createSync();
|
||||
String dateTime = DateTime.now()
|
||||
final String dateTime = DateTime.now()
|
||||
.toIso8601String()
|
||||
.replaceAll('-', '')
|
||||
.replaceAll(':', '')
|
||||
.replaceAll('T', '')
|
||||
.replaceAll('.', '');
|
||||
File logcat = File('${logDir.path}/revanced-manager_logcat_$dateTime.log');
|
||||
String logs = await Logcat.execute();
|
||||
final File logcat =
|
||||
File('${logDir.path}/revanced-manager_logcat_$dateTime.log');
|
||||
final String logs = await Logcat.execute();
|
||||
logcat.writeAsStringSync(logs);
|
||||
ShareExtend.share(logcat.path, 'file');
|
||||
}
|
||||
|
||||
@@ -8,12 +8,11 @@ import 'package:revanced_manager/ui/widgets/shared/custom_sliver_app_bar.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
|
||||
class AppInfoView extends StatelessWidget {
|
||||
final PatchedApplication app;
|
||||
|
||||
const AppInfoView({
|
||||
Key? key,
|
||||
required this.app,
|
||||
}) : super(key: key);
|
||||
final PatchedApplication app;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -28,7 +27,7 @@ class AppInfoView extends StatelessWidget {
|
||||
child: Text(
|
||||
'',
|
||||
style: GoogleFonts.inter(
|
||||
color: Theme.of(context).textTheme.headline6!.color,
|
||||
color: Theme.of(context).textTheme.titleLarge!.color,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -52,13 +51,13 @@ class AppInfoView extends StatelessWidget {
|
||||
Text(
|
||||
app.name,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.headline6,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
app.version,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.subtitle1,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Padding(
|
||||
@@ -154,43 +153,6 @@ class AppInfoView extends StatelessWidget {
|
||||
endIndent: 12.0,
|
||||
width: 1.0,
|
||||
),
|
||||
Expanded(
|
||||
child: Material(
|
||||
type: MaterialType.transparency,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(16.0),
|
||||
onTap: () {
|
||||
model.navigateToPatcher(app);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Column(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
Icons.build_outlined,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.primary,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
I18nText(
|
||||
'appInfoView.patchButton',
|
||||
child: Text(
|
||||
'',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (app.isRooted)
|
||||
VerticalDivider(
|
||||
color: Theme.of(context).canvasColor,
|
||||
|
||||
@@ -8,17 +8,18 @@ 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/root_api.dart';
|
||||
import 'package:revanced_manager/services/toast.dart';
|
||||
import 'package:revanced_manager/ui/views/home/home_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/widgets/shared/custom_material_button.dart';
|
||||
import 'package:revanced_manager/utils/string.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
|
||||
class AppInfoViewModel extends BaseViewModel {
|
||||
final ManagerAPI _managerAPI = locator<ManagerAPI>();
|
||||
final PatcherAPI _patcherAPI = locator<PatcherAPI>();
|
||||
final RootAPI _rootAPI = RootAPI();
|
||||
final Toast _toast = locator<Toast>();
|
||||
|
||||
Future<void> uninstallApp(
|
||||
BuildContext context,
|
||||
@@ -27,7 +28,7 @@ class AppInfoViewModel extends BaseViewModel {
|
||||
) async {
|
||||
bool isUninstalled = true;
|
||||
if (app.isRooted) {
|
||||
bool hasRootPermissions = await _rootAPI.hasRootPermissions();
|
||||
final bool hasRootPermissions = await _rootAPI.hasRootPermissions();
|
||||
if (hasRootPermissions) {
|
||||
await _rootAPI.deleteApp(app.packageName, app.apkFilePath);
|
||||
if (!onlyUnpatch) {
|
||||
@@ -43,7 +44,7 @@ class AppInfoViewModel extends BaseViewModel {
|
||||
}
|
||||
}
|
||||
|
||||
void navigateToPatcher(PatchedApplication app) async {
|
||||
Future<void> navigateToPatcher(PatchedApplication app) async {
|
||||
locator<PatcherViewModel>().selectedApp = app;
|
||||
locator<PatcherViewModel>().selectedPatches =
|
||||
await _patcherAPI.getAppliedPatches(app.appliedPatches);
|
||||
@@ -51,12 +52,16 @@ class AppInfoViewModel extends BaseViewModel {
|
||||
locator<NavigationViewModel>().setIndex(1);
|
||||
}
|
||||
|
||||
void updateNotImplemented(BuildContext context) {
|
||||
_toast.showBottom('appInfoView.updateNotImplemented');
|
||||
}
|
||||
|
||||
Future<void> showUninstallDialog(
|
||||
BuildContext context,
|
||||
PatchedApplication app,
|
||||
bool onlyUnpatch,
|
||||
) async {
|
||||
bool hasRootPermissions = await _rootAPI.hasRootPermissions();
|
||||
final bool hasRootPermissions = await _rootAPI.hasRootPermissions();
|
||||
if (app.isRooted && !hasRootPermissions) {
|
||||
return showDialog(
|
||||
context: context,
|
||||
@@ -68,7 +73,7 @@ class AppInfoViewModel extends BaseViewModel {
|
||||
CustomMaterialButton(
|
||||
label: I18nText('okButton'),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -97,7 +102,7 @@ class AppInfoViewModel extends BaseViewModel {
|
||||
Navigator.of(context).pop();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -127,27 +132,21 @@ class AppInfoViewModel extends BaseViewModel {
|
||||
builder: (context) => AlertDialog(
|
||||
title: I18nText('appInfoView.appliedPatchesLabel'),
|
||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
content: Text(getAppliedPatchesString(app.appliedPatches)),
|
||||
content: SingleChildScrollView(
|
||||
child: Text(getAppliedPatchesString(app.appliedPatches)),
|
||||
),
|
||||
actions: <Widget>[
|
||||
CustomMaterialButton(
|
||||
label: I18nText('okButton'),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String getAppliedPatchesString(List<String> appliedPatches) {
|
||||
List<String> names = appliedPatches
|
||||
.map((p) => p
|
||||
.replaceAll('-', ' ')
|
||||
.split('-')
|
||||
.join(' ')
|
||||
.toTitleCase()
|
||||
.replaceFirst('Microg', 'MicroG'))
|
||||
.toList();
|
||||
return '\u2022 ${names.join('\n\u2022 ')}';
|
||||
return '• ${appliedPatches.join('\n• ')}';
|
||||
}
|
||||
|
||||
void openApp(PatchedApplication app) {
|
||||
|
||||