Compare commits
447 Commits
Author | SHA1 | Date | |
---|---|---|---|
166a3180d3 | |||
3bf4982f23 | |||
f4e1cccfac | |||
7911a8f49e | |||
64a96fc3ce | |||
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 | |||
6d45ccecc2 | |||
5418c36716 | |||
ca0657e8f9 | |||
a5511c2a2c | |||
a346f8857f | |||
e12532ea4c | |||
7ecf951bfb | |||
db18874ea1 | |||
18a69776cd | |||
21cadf6450 | |||
5ddbe6e252 | |||
6d1427e01e | |||
6ac901f1d6 | |||
587ba795bb | |||
6b66c7bbd0 | |||
f398b6863a | |||
4d82ff3011 | |||
2a0ea78d7f | |||
4722880647 | |||
32fabcfa3f | |||
e0c46e4268 | |||
d84230fa22 | |||
9561153bfb | |||
c8c35ca801 | |||
cf99069804 | |||
6abb761724 | |||
4609ed9eba | |||
c0f743df89 | |||
f00f910973 | |||
2bc486dcec | |||
148981da18 | |||
d8df377427 | |||
efe1306aac | |||
ead308740f |
28
.github/ISSUE_TEMPLATE/bug-issue.yml
vendored
@ -20,14 +20,6 @@ body:
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Manager Branch
|
||||
options:
|
||||
- Flutter
|
||||
- Compose
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Bug description
|
||||
@ -37,7 +29,7 @@ body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Steps to reproduce
|
||||
description: Add the steps to reproduce this bug including your environment.
|
||||
description: Add the steps to reproduce this bug, including your environment.
|
||||
placeholder: Step 1. Download some files. Step 2. ...
|
||||
validations:
|
||||
required: true
|
||||
@ -82,20 +74,20 @@ body:
|
||||
- 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.
|
||||
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so there is no need for backticks.
|
||||
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: Installer logs (exported using Installer menu option) [unneeded if the issue is not during patching].
|
||||
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so there is no need for backticks.
|
||||
render: shell
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Screenshots or videos
|
||||
label: Screenshots or video
|
||||
description: Add screenshots or videos that show the bug here.
|
||||
placeholder: Drag and drop the screenshots/videos into this box.
|
||||
validations:
|
||||
@ -113,16 +105,16 @@ body:
|
||||
validations:
|
||||
required: false
|
||||
- type: checkboxes
|
||||
id: acknowledgements
|
||||
id: acknowledgments
|
||||
attributes:
|
||||
label: Acknowledgements
|
||||
label: Acknowledgments
|
||||
description: Your issue will be closed if you haven't done these steps.
|
||||
options:
|
||||
- label: I have searched the existing issues and this is a new and no duplicate or related to another open issue.
|
||||
- label: I have searched the existing issues; this is new and no duplicate or related to another open issue.
|
||||
required: true
|
||||
- label: I have written a short but informative title.
|
||||
required: true
|
||||
- label: I filled out all of the requested information in this issue properly.
|
||||
- label: I properly filled out all of the requested information in this issue.
|
||||
required: true
|
||||
- label: The issue is related solely to the ReVanced Manager
|
||||
- label: The issue is solely related to ReVanced Manager and not caused by patches.
|
||||
required: true
|
||||
|
8
.github/ISSUE_TEMPLATE/feature-issue.yml
vendored
@ -12,14 +12,6 @@ body:
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Manager Branch
|
||||
options:
|
||||
- Flutter
|
||||
- Compose
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Issue
|
||||
|
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.
|
38
.github/workflows/analyze.yml
vendored
Normal file
@ -0,0 +1,38 @@
|
||||
name: Analyze Code
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "dev" ]
|
||||
paths:
|
||||
- "**.dart"
|
||||
- ".github/workflows/analyze.yml"
|
||||
pull_request:
|
||||
branches: [ "main", "dev" ]
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- synchronize
|
||||
- ready_for_review
|
||||
paths:
|
||||
- "**.dart"
|
||||
- ".github/workflows/analyze.yml"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: "Static analysis & format check"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- 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: Analyze code
|
||||
uses: ValentinVignal/action-dart-analyze@v0.15
|
||||
with:
|
||||
fail-on: warning
|
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@v3
|
||||
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@v3
|
||||
- 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 }}"}'
|
8
.gitignore
vendored
@ -134,5 +134,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
|
75
.releaserc
Normal file
@ -0,0 +1,75 @@
|
||||
{
|
||||
"branches": [
|
||||
"main",
|
||||
{
|
||||
"name": "dev",
|
||||
"prerelease": true
|
||||
}
|
||||
],
|
||||
"plugins": [
|
||||
"semantic-release-export-data",
|
||||
"@semantic-release/commit-analyzer",
|
||||
[
|
||||
"@semantic-release/release-notes-generator",
|
||||
{
|
||||
"presetConfig": {
|
||||
"types": [
|
||||
{
|
||||
"type": "build",
|
||||
"section": "Dependency Updates"
|
||||
},
|
||||
{
|
||||
"type": "chore",
|
||||
"section": "Other Changes",
|
||||
"hidden": false
|
||||
},
|
||||
{
|
||||
"type": "perf",
|
||||
"section": "Performance Improvements",
|
||||
"hidden": false
|
||||
},
|
||||
{
|
||||
"type": "refactor",
|
||||
"section": "Code Improvements",
|
||||
"hidden": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"@semantic-release/changelog",
|
||||
"semantic-release-flutter-plugin",
|
||||
[
|
||||
"@semantic-release/git",
|
||||
{
|
||||
"assets": [
|
||||
"CHANGELOG.md",
|
||||
"pubspec.yaml"
|
||||
]
|
||||
}
|
||||
],
|
||||
[
|
||||
"@semantic-release/github",
|
||||
{
|
||||
"assets": [
|
||||
{
|
||||
"path": "build/app/outputs/apk/release/revanced-manager-*.apk"
|
||||
}
|
||||
],
|
||||
"successComment": false
|
||||
}
|
||||
],
|
||||
[
|
||||
"@saithodev/semantic-release-backmerge",
|
||||
{
|
||||
"backmergeBranches": [
|
||||
{
|
||||
"from": "main",
|
||||
"to": "dev"
|
||||
}
|
||||
],
|
||||
"clearWorkspace": true
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
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>
|
1
CHANGELOG.md
Normal file
@ -0,0 +1 @@
|
||||
|
29
README.md
@ -3,18 +3,29 @@
|
||||
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.
|
||||
To download latest Manager, go [here](https://github.com/revanced/revanced-manager/releases/latest) and install the provided APK file.
|
||||
|
||||
## 📝 Prerequisites
|
||||
1. Android 8 or higher
|
||||
2. Does not work on some armv7 devices
|
||||
|
||||
## 🔴 Issues
|
||||
For suggestions and bug reports, open an issue [here](https://github.com/revanced/revanced-manager/issues/new/choose).
|
||||
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.
|
||||
If you wish to discuss the Manager, a thread has been made under the [#development](https://discord.com/channels/952946952348270622/1002922226443632761) channel in the Discord server, please note that this thread may be temporary and may be removed in the future.
|
||||
|
||||
## ⚠️ 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.*
|
||||
|
||||
## Prerequisites
|
||||
1. Android 8 or higher.
|
||||
2. Does not work on armv7
|
||||
3. For YouTube and YouTube Music - Vanced MicroG(Only for non-root).
|
||||
## 🌐 Translation
|
||||
[](https://crowdin.com/project/revanced)
|
||||
|
||||
If you wish to translate ReVanced Manager, 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.0.1"
|
||||
implementation "app.revanced:revanced-patcher:14.2.1"
|
||||
|
||||
// Signing & aligning
|
||||
implementation("org.bouncycastle:bcpkix-jdk15on:1.70")
|
||||
|
@ -2,22 +2,31 @@
|
||||
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:extractNativeLibs="true">
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:extractNativeLibs="true"
|
||||
android:enableOnBackInvokedCallback="true">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
@ -28,8 +37,7 @@
|
||||
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"/>
|
||||
|
BIN
android/app/src/main/jniLibs/arm64-v8a/libaapt2.so
Normal file
BIN
android/app/src/main/jniLibs/armeabi-v7a/libaapt2.so
Normal file
BIN
android/app/src/main/jniLibs/x86_64/libaapt2.so
Normal file
@ -2,34 +2,47 @@ package app.revanced.manager.flutter
|
||||
|
||||
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.Patcher
|
||||
import app.revanced.patcher.PatcherOptions
|
||||
import app.revanced.patcher.extensions.PatchExtensions.compatiblePackages
|
||||
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 java.io.File
|
||||
import java.io.PrintWriter
|
||||
import java.io.StringWriter
|
||||
import java.util.logging.LogRecord
|
||||
import java.util.logging.Logger
|
||||
|
||||
private const val PATCHER_CHANNEL = "app.revanced.manager.flutter/patcher"
|
||||
private const val INSTALLER_CHANNEL = "app.revanced.manager.flutter/installer"
|
||||
|
||||
class MainActivity : FlutterActivity() {
|
||||
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) {
|
||||
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" -> {
|
||||
@ -41,9 +54,9 @@ class MainActivity : FlutterActivity() {
|
||||
val integrationsPath = call.argument<String>("integrationsPath")
|
||||
val selectedPatches = call.argument<List<String>>("selectedPatches")
|
||||
val cacheDirPath = call.argument<String>("cacheDirPath")
|
||||
val mergeIntegrations = call.argument<Boolean>("mergeIntegrations")
|
||||
val resourcePatching = call.argument<Boolean>("resourcePatching")
|
||||
val keyStoreFilePath = call.argument<String>("keyStoreFilePath")
|
||||
val keystorePassword = call.argument<String>("keystorePassword")
|
||||
|
||||
if (patchBundleFilePath != null &&
|
||||
originalFilePath != null &&
|
||||
inputFilePath != null &&
|
||||
@ -52,10 +65,10 @@ class MainActivity : FlutterActivity() {
|
||||
integrationsPath != null &&
|
||||
selectedPatches != null &&
|
||||
cacheDirPath != null &&
|
||||
mergeIntegrations != null &&
|
||||
resourcePatching != null &&
|
||||
keyStoreFilePath != null
|
||||
keyStoreFilePath != null &&
|
||||
keystorePassword != null
|
||||
) {
|
||||
cancel = false
|
||||
runPatcher(
|
||||
result,
|
||||
patchBundleFilePath,
|
||||
@ -66,14 +79,17 @@ class MainActivity : FlutterActivity() {
|
||||
integrationsPath,
|
||||
selectedPatches,
|
||||
cacheDirPath,
|
||||
mergeIntegrations,
|
||||
resourcePatching,
|
||||
keyStoreFilePath
|
||||
keyStoreFilePath,
|
||||
keystorePassword
|
||||
)
|
||||
} else {
|
||||
result.notImplemented()
|
||||
}
|
||||
} else result.notImplemented()
|
||||
}
|
||||
|
||||
"stopPatcher" -> {
|
||||
cancel = true
|
||||
stopResult = result
|
||||
}
|
||||
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
@ -89,9 +105,8 @@ class MainActivity : FlutterActivity() {
|
||||
integrationsPath: String,
|
||||
selectedPatches: List<String>,
|
||||
cacheDirPath: String,
|
||||
mergeIntegrations: Boolean,
|
||||
resourcePatching: Boolean,
|
||||
keyStoreFilePath: String
|
||||
keyStoreFilePath: String,
|
||||
keystorePassword: String
|
||||
) {
|
||||
val originalFile = File(originalFilePath)
|
||||
val inputFile = File(inputFilePath)
|
||||
@ -99,122 +114,142 @@ class MainActivity : FlutterActivity() {
|
||||
val outFile = File(outFilePath)
|
||||
val integrations = File(integrationsPath)
|
||||
val keyStoreFile = File(keyStoreFilePath)
|
||||
val cacheDir = File(cacheDirPath)
|
||||
|
||||
Thread {
|
||||
try {
|
||||
val patches = DexPatchBundle(
|
||||
patchBundleFilePath,
|
||||
DexClassLoader(
|
||||
patchBundleFilePath,
|
||||
cacheDirPath,
|
||||
null,
|
||||
javaClass.classLoader
|
||||
)
|
||||
).loadPatches().filter { patch -> selectedPatches.any { it == patch.patchName } }
|
||||
|
||||
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) =
|
||||
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"
|
||||
)
|
||||
)
|
||||
}
|
||||
val patcher =
|
||||
Patcher(
|
||||
PatcherOptions(
|
||||
inputFile,
|
||||
cacheDirPath,
|
||||
resourcePatching,
|
||||
Aapt.binary(applicationContext).absolutePath,
|
||||
cacheDirPath,
|
||||
logger = ManagerLogger()
|
||||
)
|
||||
)
|
||||
|
||||
handler.post {
|
||||
installerChannel.invokeMethod(
|
||||
"update",
|
||||
mapOf("progress" to 0.3, "header" to "", "log" to "")
|
||||
)
|
||||
}
|
||||
if (mergeIntegrations) {
|
||||
handler.post {
|
||||
installerChannel.invokeMethod(
|
||||
"update",
|
||||
mapOf(
|
||||
"progress" to 0.4,
|
||||
"header" to "Merging integrations...",
|
||||
"log" to "Merging integrations"
|
||||
)
|
||||
)
|
||||
}
|
||||
patcher.addFiles(listOf(integrations)) {}
|
||||
if (cancel) {
|
||||
postStop()
|
||||
return@Thread
|
||||
}
|
||||
|
||||
handler.post {
|
||||
installerChannel.invokeMethod(
|
||||
"update",
|
||||
mapOf(
|
||||
"progress" to 0.5,
|
||||
"header" to "Applying patches...",
|
||||
"log" to ""
|
||||
)
|
||||
updateProgress(0.05, "Reading APK...", "Reading APK")
|
||||
|
||||
val patcher = Patcher(
|
||||
PatcherOptions(
|
||||
inputFile,
|
||||
cacheDir,
|
||||
Aapt.binary(applicationContext).absolutePath,
|
||||
cacheDir.path,
|
||||
)
|
||||
)
|
||||
|
||||
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.1, "Loading patches...", "Loading patches")
|
||||
|
||||
val patches = PatchBundleLoader.Dex(
|
||||
File(patchBundleFilePath)
|
||||
).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.patchName }
|
||||
}
|
||||
|
||||
if (cancel) {
|
||||
postStop()
|
||||
return@Thread
|
||||
}
|
||||
|
||||
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.patchName} failed: $writer"
|
||||
} ?: run {
|
||||
"${patchResult.patchName} 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 +266,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,
|
||||
"Aborted",
|
||||
"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()
|
||||
@ -22,6 +22,7 @@ allprojects {
|
||||
password = (project.findProperty("gpr.key") ?: System.getenv("GITHUB_TOKEN")) as String
|
||||
}
|
||||
}
|
||||
mavenLocal()
|
||||
}
|
||||
}
|
||||
|
||||
@ -31,6 +32,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,7 @@
|
||||
#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
|
||||
distributionSha256Sum=a01b6587e15fe7ed120a0ee299c25982a1eee045abd6a9dd5e216b2f628ef9ac
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.2-bin.zip
|
||||
networkTimeout=10000
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
@ -1,159 +0,0 @@
|
||||
{
|
||||
"okButton": "OK",
|
||||
"cancelButton": "Cancel",
|
||||
"enabledLabel": "Enabled",
|
||||
"disabledLabel": "Disabled",
|
||||
"yesButton": "Yes",
|
||||
"noButton": "No",
|
||||
"navigationView": {
|
||||
"dashboardTab": "Dashboard",
|
||||
"patcherTab": "Patcher",
|
||||
"settingsTab": "Settings"
|
||||
},
|
||||
"homeView": {
|
||||
"widgetTitle": "Dashboard",
|
||||
"updatesSubtitle": "Updates",
|
||||
"patchedSubtitle": "Patched Applications",
|
||||
"updatesAvailable": "Updates available",
|
||||
"noUpdates": "No updates available",
|
||||
"noInstallations": "No patched applications installed",
|
||||
"installed": "Installed",
|
||||
"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",
|
||||
"downloadingMessage": "Downloading update...",
|
||||
"installingMessage": "Installing update...",
|
||||
"errorDownloadMessage": "Unable to download update",
|
||||
"errorInstallMessage": "Unable to install update",
|
||||
"noConnection": "No internet connection"
|
||||
},
|
||||
"applicationItem": {
|
||||
"patchButton": "Patch",
|
||||
"infoButton": "Info",
|
||||
"changelogLabel": "Changelog"
|
||||
},
|
||||
"latestCommitCard": {
|
||||
"loadingLabel": "Loading...",
|
||||
"timeagoLabel": "{time} ago",
|
||||
"patcherLabel": "Patcher: ",
|
||||
"managerLabel": "Manager: ",
|
||||
"updateButton": "Update Manager"
|
||||
},
|
||||
"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?"
|
||||
},
|
||||
"appSelectorCard": {
|
||||
"widgetTitle": "Select application",
|
||||
"widgetTitleSelected": "Selected application",
|
||||
"widgetSubtitle": "No application selected",
|
||||
"noAppsLabel": "No applications found.",
|
||||
"currentVersion": "Current",
|
||||
"recommendedVersion": "Recommended",
|
||||
"anyVersion": "any"
|
||||
},
|
||||
"patchSelectorCard": {
|
||||
"widgetTitle": "Select patches",
|
||||
"widgetTitleSelected": "Selected patches",
|
||||
"widgetSubtitle": "Select an application first",
|
||||
"widgetEmptySubtitle": "No patches selected"
|
||||
},
|
||||
"socialMediaCard": {
|
||||
"widgetTitle": "Socials",
|
||||
"widgetSubtitle": "We are online!"
|
||||
},
|
||||
"appSelectorView": {
|
||||
"viewTitle": "Select application",
|
||||
"searchBarHint": "Search applications",
|
||||
"storageButton": "Storage",
|
||||
"errorMessage": "Unable to use selected application."
|
||||
},
|
||||
"patchesSelectorView": {
|
||||
"viewTitle": "Select patches",
|
||||
"searchBarHint": "Search patches",
|
||||
"doneButton": "Done",
|
||||
"noPatchesFound": "No patches found for the selected app"
|
||||
},
|
||||
"patchItem": {
|
||||
"unsupportedWarningButton": "Unsupported version",
|
||||
"unsupportedDialogTitle": "Warning",
|
||||
"unsupportedDialogText": "Selecting this patch may result in patching errors.\n\nApp version: {packageVersion}\nCurrent supported versions:\n{supportedVersions}"
|
||||
},
|
||||
"installerView": {
|
||||
"widgetTitle": "Installer",
|
||||
"installButton": "Install",
|
||||
"installRootButton": "Install as Root",
|
||||
"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..."
|
||||
},
|
||||
"settingsView": {
|
||||
"widgetTitle": "Settings",
|
||||
"appearanceSectionTitle": "Appearance",
|
||||
"teamSectionTitle": "Team",
|
||||
"infoSectionTitle": "Info",
|
||||
"advancedSectionTitle": "Advanced",
|
||||
"darkThemeLabel": "Dark Mode",
|
||||
"darkThemeHint": "Welcome to the Dark Side",
|
||||
"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",
|
||||
"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?",
|
||||
"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",
|
||||
"aboutLabel": "About",
|
||||
"snackbarMessage": "Copied to clipboard"
|
||||
},
|
||||
"appInfoView": {
|
||||
"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",
|
||||
"rootTypeLabel": "Root",
|
||||
"nonRootTypeLabel": "Non-root",
|
||||
"patchedDateLabel": "Patched Date",
|
||||
"patchedDateHint": "{date} at {time}",
|
||||
"appliedPatchesLabel": "Applied Patches",
|
||||
"appliedPatchesHint": "{quantity} applied patches"
|
||||
},
|
||||
"contributorsView": {
|
||||
"widgetTitle": "Contributors",
|
||||
"patcherContributors": "Patcher Contributors",
|
||||
"patchesContributors": "Patches Contributors",
|
||||
"integrationsContributors": "Integrations Contributors",
|
||||
"cliContributors": "CLI Contributors",
|
||||
"managerContributors": "Manager Contributors"
|
||||
}
|
||||
}
|
306
assets/i18n/en_US.json
Normal file
@ -0,0 +1,306 @@
|
||||
{
|
||||
"okButton": "OK",
|
||||
"cancelButton": "Cancel",
|
||||
"quitButton": "Quit",
|
||||
"updateButton": "Update",
|
||||
"enabledLabel": "Enabled",
|
||||
"disabledLabel": "Disabled",
|
||||
"installed":"Installed: {version}",
|
||||
"suggested":"Suggested: {version}",
|
||||
"yesButton": "Yes",
|
||||
"noButton": "No",
|
||||
"warning": "Warning",
|
||||
"notice": "Notice",
|
||||
"noShowAgain": "Don't show this again",
|
||||
"new": "New",
|
||||
"navigationView": {
|
||||
"dashboardTab": "Dashboard",
|
||||
"patcherTab": "Patcher",
|
||||
"settingsTab": "Settings"
|
||||
},
|
||||
"homeView": {
|
||||
"refreshSuccess": "Refreshed successfully",
|
||||
"widgetTitle": "Dashboard",
|
||||
|
||||
"updatesSubtitle": "Updates",
|
||||
"patchedSubtitle": "Patched applications",
|
||||
|
||||
"noUpdates": "No updates available",
|
||||
|
||||
"WIP": "Work in progress...",
|
||||
|
||||
"noInstallations": "No patched applications installed",
|
||||
"installUpdate": "Continue to install the update?",
|
||||
|
||||
"updateDialogTitle": "Update Manager",
|
||||
"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",
|
||||
"updatesDisabled": "Updating a patched app is currently disabled. Repatch the app again."
|
||||
},
|
||||
"applicationItem": {
|
||||
"patchButton": "Patch",
|
||||
"infoButton": "Info",
|
||||
"changelogLabel": "Changelog"
|
||||
},
|
||||
"latestCommitCard": {
|
||||
"loadingLabel": "Loading...",
|
||||
"timeagoLabel": "{time} ago",
|
||||
"patcherLabel": "Patcher: ",
|
||||
"managerLabel": "Manager: ",
|
||||
"updateButton": "Update Manager"
|
||||
},
|
||||
"patcherView": {
|
||||
"widgetTitle": "Patcher",
|
||||
"patchButton": "Patch",
|
||||
|
||||
"patchDialogText": "You have selected a resource patch and a split APK installation has been detected, so patching errors may occur.\nAre you sure you want to proceed?",
|
||||
"armv7WarningDialogText": "Patching on ARMv7 devices is not yet supported and might fail. Proceed anyways?",
|
||||
"splitApkWarningDialogText": "Patching a split APK 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?"
|
||||
},
|
||||
"appSelectorCard": {
|
||||
"widgetTitle": "Select an application",
|
||||
"widgetTitleSelected": "Selected application",
|
||||
"widgetSubtitle": "No application selected",
|
||||
|
||||
"noAppsLabel": "No applications found",
|
||||
"notInstalled":"App not installed",
|
||||
|
||||
"currentVersion": "Current",
|
||||
"suggestedVersion": "Suggested",
|
||||
"allVersions": "All versions"
|
||||
},
|
||||
"patchSelectorCard": {
|
||||
"widgetTitle": "Select patches",
|
||||
"widgetTitleSelected": "Selected patches",
|
||||
|
||||
"widgetSubtitle": "Select an application first",
|
||||
"widgetEmptySubtitle": "No patches selected"
|
||||
},
|
||||
"socialMediaCard": {
|
||||
"widgetTitle": "Socials",
|
||||
"widgetSubtitle": "We are online!"
|
||||
},
|
||||
"appSelectorView": {
|
||||
"viewTitle": "Select an application",
|
||||
"searchBarHint": "Search applications",
|
||||
|
||||
"storageButton": "Storage",
|
||||
"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",
|
||||
|
||||
"doneButton": "Done",
|
||||
|
||||
"default": "Default",
|
||||
"defaultTooltip": "Select all default patches",
|
||||
|
||||
"none": "None",
|
||||
"noneTooltip": "Deselect all patches",
|
||||
|
||||
"loadPatchesSelection": "Load patches selection",
|
||||
"noSavedPatches": "No saved patches for the selected app.\nPress Done to save current selection.",
|
||||
"noPatchesFound": "No patches found for the selected app",
|
||||
|
||||
"selectAllPatchesWarningContent": "You are about to select all patches, that includes non-suggested patches and can cause unwanted behavior."
|
||||
},
|
||||
"patchItem": {
|
||||
"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. Enable the experimental toggle in settings to proceed.",
|
||||
|
||||
"newPatchDialogText": "This is a new patch that has been added since the last time you have patched this app.",
|
||||
"newPatch": "New patch",
|
||||
|
||||
"patchesChangeWarningDialogText": "It is recommended to use the default selection of patches because changing it may cause unexpected issues.\n\nIf you know what you are doing, you can enable \"Enable changing selection\" in the settings.",
|
||||
"patchesChangeWarningDialogButton": "Use default selection"
|
||||
},
|
||||
"installerView": {
|
||||
"widgetTitle": "Installer",
|
||||
"installType": "Select install type",
|
||||
"installTypeDescription": "Select the installation type to proceed with.",
|
||||
|
||||
"installButton": "Install",
|
||||
"installRootType": "Root",
|
||||
"installNonRootType": "Non-root",
|
||||
"installRecommendedType": "Recommended",
|
||||
|
||||
"pressBackAgain": "Press back again to cancel",
|
||||
"openButton": "Open",
|
||||
"shareButton": "Share file",
|
||||
|
||||
"notificationTitle": "ReVanced Manager is patching",
|
||||
"notificationText": "Tap to return to the installer",
|
||||
|
||||
"exportApkButtonTooltip": "Export patched APK",
|
||||
"exportLogButtonTooltip": "Export 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, cannot exit..."
|
||||
},
|
||||
"settingsView": {
|
||||
"widgetTitle": "Settings",
|
||||
|
||||
"appearanceSectionTitle": "Appearance",
|
||||
"teamSectionTitle": "Team",
|
||||
"infoSectionTitle": "Info",
|
||||
"advancedSectionTitle": "Advanced",
|
||||
"exportSectionTitle": "Import & export",
|
||||
"logsSectionTitle": "Logs",
|
||||
|
||||
"darkThemeLabel": "Dark mode",
|
||||
"darkThemeHint": "Welcome to the dark side",
|
||||
|
||||
"dynamicThemeLabel": "Material You",
|
||||
"dynamicThemeHint": "Enjoy an experience closer to your device",
|
||||
|
||||
"languageLabel": "Language",
|
||||
"englishOption": "English",
|
||||
|
||||
"sourcesLabel": "Sources",
|
||||
"sourcesLabelHint": "Configure your custom 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?",
|
||||
"sourcesUpdateNote": "Note: ReVanced 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 custom 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 Manager's logs",
|
||||
|
||||
"enablePatchesSelectionLabel": "Enable changing selection",
|
||||
"enablePatchesSelectionHint": "Enable changing the selection of patches.",
|
||||
"enablePatchesSelectionWarningText": "Changing the default 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 ReVanced Patches to the latest version",
|
||||
"experimentalUniversalPatchesLabel": "Experimental universal patches support",
|
||||
"experimentalUniversalPatchesHint": "Display all applications to use with universal patches, loading list of apps may be slower",
|
||||
"experimentalPatchesLabel": "Experimental patches support",
|
||||
"experimentalPatchesHint": "Enable usage of unsupported patches in any app version",
|
||||
"enabledExperimentalPatches": "Experimental patches support enabled",
|
||||
|
||||
"aboutLabel": "About",
|
||||
"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 patches selection",
|
||||
"exportPatchesHint": "Export patches selection to a JSON file",
|
||||
"exportedPatches": "Patches selection exported",
|
||||
"noExportFileFound": "No patches selection to export",
|
||||
|
||||
"importPatchesLabel": "Import patches selection",
|
||||
"importPatchesHint": "Import patches selection from a JSON file",
|
||||
"importedPatches": "Patches selection imported",
|
||||
|
||||
"resetStoredPatchesLabel": "Reset patches",
|
||||
"resetStoredPatchesHint": "Reset the stored patches selection",
|
||||
|
||||
"resetStoredPatchesDialogTitle": "Reset patches selection?",
|
||||
"resetStoredPatchesDialogText": "Resetting patches selection will remove all selected patches.",
|
||||
"resetStoredPatches": "Patches selection has been reset",
|
||||
|
||||
"deleteLogsLabel": "Delete logs",
|
||||
"deleteLogsHint": "Delete collected manager logs",
|
||||
"deletedLogs": "Logs deleted",
|
||||
|
||||
"regenerateKeystoreLabel": "Regenerate keystore",
|
||||
"regenerateKeystoreHint": "Regenerate the keystore used to sign the app",
|
||||
|
||||
"regenerateKeystoreDialogTitle": "Regenerate keystore?",
|
||||
"regenerateKeystoreDialogText": "Patched apps signed with the old keystore will no longer be able to update.",
|
||||
"regeneratedKeystore": "Keystore regenerated",
|
||||
|
||||
"exportKeystoreLabel": "Export keystore",
|
||||
"exportKeystoreHint": "Export keystore used to sign apps",
|
||||
"exportedKeystore": "Keystore exported",
|
||||
"noKeystoreExportFileFound": "No keystore to export",
|
||||
|
||||
"importKeystoreLabel": "Import keystore",
|
||||
"importKeystoreHint": "Import keystore used to sign apps",
|
||||
"importedKeystore": "Keystore imported",
|
||||
|
||||
"selectKeystorePassword": "Keystore Password",
|
||||
"selectKeystorePasswordHint": "Select keystore password used to sign the apk",
|
||||
|
||||
"jsonSelectorErrorMessage": "Unable to use selected JSON file",
|
||||
"keystoreSelectorErrorMessage": "Unable to use selected KEYSTORE file"
|
||||
},
|
||||
"appInfoView": {
|
||||
"widgetTitle": "App info",
|
||||
"openButton": "Open",
|
||||
"uninstallButton": "Uninstall",
|
||||
"unpatchButton": "Unpatch",
|
||||
"rootDialogTitle": "Error",
|
||||
|
||||
"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",
|
||||
"originalPackageNameLabel": "Original package name",
|
||||
"installTypeLabel": "Installation type",
|
||||
"rootTypeLabel": "Root",
|
||||
"nonRootTypeLabel": "Non-root",
|
||||
"patchedDateLabel": "Patched date",
|
||||
"appliedPatchesLabel": "Applied patches",
|
||||
|
||||
"patchedDateHint": "{date} at {time}",
|
||||
"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"
|
||||
}
|
||||
}
|
10
crowdin.yml
@ -1,3 +1,9 @@
|
||||
project_id_env: CROWDIN_PROJECT_ID
|
||||
api_token_env: CROWDIN_PERSONAL_TOKEN
|
||||
|
||||
commit_message: 'chore(i18n): sync translations'
|
||||
|
||||
preserve_hierarchy: true
|
||||
files:
|
||||
- source: /assets/i18n/en.json
|
||||
translation: /assets/i18n/%two_letters_code%.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)
|
27
docs/2_1_patching.md
Normal file
@ -0,0 +1,27 @@
|
||||
# 🧩 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]
|
||||
> **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 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 options in the top right corner.
|
||||
|
||||
[^1]: Non-root users may be prompted to select an APK from storage, in which case you have to 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)
|
39
docs/2_4_settings.md
Normal file
@ -0,0 +1,39 @@
|
||||
# ⚙️ Configuring ReVanced Manager
|
||||
|
||||
ReVanced Manager has settings that can be configured to your liking.
|
||||
|
||||
## ⭐ Essential settings
|
||||
|
||||
- ### 🔗 API URL
|
||||
|
||||
Specify the URL of the API to use. This is used to fetch ReVanced Patches and update ReVanced Manager.
|
||||
|
||||
- ### 🧬 Sources
|
||||
|
||||
Override the API and change the source of ReVanced Patches.
|
||||
|
||||
- ### 🧪 Experimental ReVanced Patches support
|
||||
|
||||
Lift app version constraints from ReVanced Patches. This allows you to patch any version of an app, even if the patch is not explicitly compatible with it.
|
||||
|
||||
- ### 🧑🔬 Experimental universal support
|
||||
|
||||
This will show or hide ReVanced Patches, which are not meant for any app in particular but rather for all apps but may not work on all apps.
|
||||
|
||||
- ### 🔑 Export, import or delete keystore
|
||||
|
||||
Manage the keystore used to sign patched apps.
|
||||
|
||||
- ### 📄 Export, import or reset ReVanced Patches selection
|
||||
|
||||
Manage the ReVanced Patches selection. This is useful if you want to share your ReVanced Patches selection with others or reset it to the default selection.
|
||||
|
||||
- ### ℹ️ About
|
||||
|
||||
View information about your device and ReVanced Manager. This includes the version of ReVanced Manager and supported architectures of your device.
|
||||
|
||||
## ⏭️ What's next
|
||||
|
||||
The next page will bring you back to the usage page.
|
||||
|
||||
Continue: [🛠️ Usage](2_usage.md)
|
16
docs/2_usage.md
Normal file
@ -0,0 +1,16 @@
|
||||
# 🛠️ Usage
|
||||
|
||||
The following pages will guide you through using ReVanced Manager to patch apps, manage patched apps, and update ReVanced Manager.
|
||||
|
||||
## 📖 Table of contents
|
||||
|
||||
1. [🧩 Patching apps](2_1_patching.md)
|
||||
2. [🧰 Managing patched apps](2_2_managing.md)
|
||||
3. [🔄 Updating ReVanced Manager](2_3_updating.md)
|
||||
4. [⚙️ Configuring ReVanced Manager](2_4_settings.md)
|
||||
|
||||
## ⏭️ What's next
|
||||
|
||||
The next page will guide you through troubleshooting ReVanced Manager.
|
||||
|
||||
Continue: [❔ Troubleshooting](3_troubleshooting.md)
|
31
docs/3_troubleshooting.md
Normal file
@ -0,0 +1,31 @@
|
||||
# ❔ Troubleshooting
|
||||
|
||||
In case you encounter any issues while using ReVanced Manager, please refer to this page for possible solutions.
|
||||
|
||||
- 💉 Patching fails with an error
|
||||
|
||||
Make sure ReVanced Manager is up to date by following [🔄 Updating ReVanced Manager](2_3_updating.md) and select the **Default** button when choosing patches.
|
||||
|
||||
- 💥 App not installed as package conflicts with an existing package
|
||||
|
||||
An existing installation of the app you're trying to patch 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.
|
||||
|
||||
- 🚫 Non-root install is not possible with the current patches selection
|
||||
|
||||
Select the **Default** button when choosing patches.
|
||||
|
||||
- 🚨 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)
|
40
docs/4_building.md
Normal file
@ -0,0 +1,40 @@
|
||||
# 🛠️ 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 `~/.gradle/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
@ -1,26 +1,31 @@
|
||||
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/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();
|
||||
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,18 +34,31 @@ 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(),
|
||||
localizationsDelegates: [
|
||||
FlutterI18nDelegate(
|
||||
translationLoader: FileTranslationLoader(
|
||||
fallbackFile: 'en',
|
||||
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,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
@ -5,23 +5,20 @@ 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,
|
||||
});
|
||||
|
||||
factory Patch.fromJson(Map<String, dynamic> json) => _$PatchFromJson(json);
|
||||
final String name;
|
||||
final String description;
|
||||
final bool excluded;
|
||||
final List<String> dependencies;
|
||||
final List<Package> compatiblePackages;
|
||||
|
||||
Map<String, dynamic> toJson() => _$PatchToJson(this);
|
||||
|
||||
@ -37,9 +34,6 @@ class Patch {
|
||||
|
||||
@JsonSerializable()
|
||||
class Package {
|
||||
final String name;
|
||||
final List<String> versions;
|
||||
|
||||
Package({
|
||||
required this.name,
|
||||
required this.versions,
|
||||
@ -47,6 +41,8 @@ class Package {
|
||||
|
||||
factory Package.fromJson(Map<String, dynamic> json) =>
|
||||
_$PackageFromJson(json);
|
||||
final String name;
|
||||
final List<String> versions;
|
||||
|
||||
Map toJson() => _$PackageToJson(this);
|
||||
}
|
||||
|
@ -6,8 +6,26 @@ part 'patched_application.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class PatchedApplication {
|
||||
PatchedApplication({
|
||||
required this.name,
|
||||
required this.packageName,
|
||||
required this.originalPackageName,
|
||||
required this.version,
|
||||
required this.apkFilePath,
|
||||
required this.icon,
|
||||
required this.patchDate,
|
||||
this.isRooted = false,
|
||||
this.isFromStorage = false,
|
||||
this.hasUpdates = false,
|
||||
this.appliedPatches = const [],
|
||||
this.changelog = const [],
|
||||
});
|
||||
|
||||
factory PatchedApplication.fromJson(Map<String, dynamic> json) =>
|
||||
_$PatchedApplicationFromJson(json);
|
||||
String name;
|
||||
String packageName;
|
||||
String originalPackageName;
|
||||
String version;
|
||||
final String apkFilePath;
|
||||
@JsonKey(
|
||||
@ -22,23 +40,6 @@ class PatchedApplication {
|
||||
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);
|
||||
|
||||
static Uint8List decodeBase64(String icon) => base64.decode(icon);
|
||||
|
@ -2,19 +2,25 @@ 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:dio_cache_interceptor/dio_cache_interceptor.dart';
|
||||
import 'package:flutter/foundation.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/models/patch.dart';
|
||||
import 'package:revanced_manager/services/manager_api.dart';
|
||||
|
||||
@lazySingleton
|
||||
class GithubAPI {
|
||||
final Dio _dio = Dio(BaseOptions(baseUrl: 'https://api.github.com'));
|
||||
final DioCacheManager _dioCacheManager = DioCacheManager(CacheConfig());
|
||||
final Options _cacheOptions = buildCacheOptions(
|
||||
const Duration(days: 1),
|
||||
maxStale: const Duration(days: 7),
|
||||
late Dio _dio = Dio();
|
||||
late final ManagerAPI _managerAPI = locator<ManagerAPI>();
|
||||
|
||||
final _cacheOptions = CacheOptions(
|
||||
store: MemCacheStore(),
|
||||
maxStale: const Duration(days: 1),
|
||||
priority: CachePriority.high,
|
||||
);
|
||||
|
||||
final Map<String, String> repoAppPath = {
|
||||
'com.google.android.youtube': 'youtube',
|
||||
'com.google.android.apps.youtube.music': 'music',
|
||||
@ -23,24 +29,115 @@ class GithubAPI {
|
||||
'com.zhiliaoapp.musically': 'tiktok',
|
||||
'de.dwd.warnapp': 'warnwetter',
|
||||
'com.garzotto.pflotsh.ecmwf_a': 'ecmwf',
|
||||
'com.spotify.music': 'spotify',
|
||||
};
|
||||
|
||||
void initialize() {
|
||||
_dio.interceptors.add(_dioCacheManager.interceptor);
|
||||
Future<void> initialize(String repoUrl) async {
|
||||
try {
|
||||
_dio = Dio(
|
||||
BaseOptions(
|
||||
baseUrl: repoUrl,
|
||||
),
|
||||
);
|
||||
|
||||
_dio.interceptors.add(DioCacheInterceptor(options: _cacheOptions));
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> clearAllCache() async {
|
||||
await _dioCacheManager.clearAll();
|
||||
try {
|
||||
await _cacheOptions.store!.clean();
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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[0];
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>?> getPatchesRelease(
|
||||
String repoName,
|
||||
String version,
|
||||
) async {
|
||||
try {
|
||||
final response = await _dio.get(
|
||||
'/repos/$repoName/releases/tags/$version',
|
||||
);
|
||||
return response.data;
|
||||
} on Exception {
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>?> getLatestPatchesRelease(
|
||||
String repoName,
|
||||
) async {
|
||||
try {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -50,33 +147,41 @@ class GithubAPI {
|
||||
String repoName,
|
||||
DateTime since,
|
||||
) async {
|
||||
String path =
|
||||
final String path =
|
||||
'src/main/kotlin/app/revanced/patches/${repoAppPath[packageName]}';
|
||||
try {
|
||||
var response = await _dio.get(
|
||||
final response = await _dio.get(
|
||||
'/repos/$repoName/commits',
|
||||
queryParameters: {
|
||||
'path': path,
|
||||
'per_page': 3,
|
||||
'since': since.toIso8601String(),
|
||||
},
|
||||
options: _cacheOptions,
|
||||
);
|
||||
List<dynamic> commits = response.data;
|
||||
final List<dynamic> commits = response.data;
|
||||
return commits
|
||||
.map((commit) =>
|
||||
(commit['commit']['message'] as String).split('\n')[0])
|
||||
.map(
|
||||
(commit) => commit['commit']['message'].split('\n')[0] +
|
||||
' - ' +
|
||||
commit['commit']['author']['name'] +
|
||||
'\n' as String,
|
||||
)
|
||||
.toList();
|
||||
} on Exception {
|
||||
return List.empty();
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
Future<File?> getLatestReleaseFile(String extension, String repoName) async {
|
||||
Future<File?> getLatestReleaseFile(
|
||||
String extension,
|
||||
String repoName,
|
||||
) async {
|
||||
try {
|
||||
Map<String, dynamic>? release = await _getLatestRelease(repoName);
|
||||
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),
|
||||
);
|
||||
@ -86,23 +191,78 @@ class GithubAPI {
|
||||
);
|
||||
}
|
||||
}
|
||||
} on Exception {
|
||||
return null;
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<List<Patch>> getPatches(String repoName) async {
|
||||
Future<File?> getPatchesReleaseFile(
|
||||
String extension,
|
||||
String repoName,
|
||||
String version,
|
||||
String url,
|
||||
) async {
|
||||
try {
|
||||
if (url.isNotEmpty) {
|
||||
return await DefaultCacheManager().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 if (extension == '.json') {
|
||||
_managerAPI.setPatchesDownloadURL(downloadUrl, false);
|
||||
} else {
|
||||
_managerAPI.setPatchesDownloadURL(downloadUrl, true);
|
||||
}
|
||||
return await DefaultCacheManager().getSingleFile(
|
||||
downloadUrl,
|
||||
);
|
||||
}
|
||||
}
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<List<Patch>> getPatches(
|
||||
String repoName,
|
||||
String version,
|
||||
String url,
|
||||
) async {
|
||||
List<Patch> patches = [];
|
||||
try {
|
||||
File? f = await getLatestReleaseFile('.json', repoName);
|
||||
final File? f = await getPatchesReleaseFile(
|
||||
'.json',
|
||||
repoName,
|
||||
version,
|
||||
url,
|
||||
);
|
||||
if (f != null) {
|
||||
List<dynamic> list = jsonDecode(f.readAsStringSync());
|
||||
final List<dynamic> list = jsonDecode(f.readAsStringSync());
|
||||
patches = list.map((patch) => Patch.fromJson(patch)).toList();
|
||||
}
|
||||
} on Exception {
|
||||
return List.empty();
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
|
||||
return patches;
|
||||
}
|
||||
}
|
||||
|
@ -1,15 +1,22 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:device_apps/device_apps.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/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 +26,33 @@ class ManagerAPI {
|
||||
final String patcherRepo = 'revanced-patcher';
|
||||
final String cliRepo = 'revanced-cli';
|
||||
late SharedPreferences _prefs;
|
||||
String defaultApiUrl = 'https://revanced-releases-api.afterst0rm.xyz';
|
||||
bool isRooted = 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() == 'revanced/revanced-patches';
|
||||
}
|
||||
|
||||
bool isDefaultIntegrationsRepo() {
|
||||
return getIntegrationsRepo() == 'revanced/revanced-integrations';
|
||||
}
|
||||
|
||||
Future<void> initialize() async {
|
||||
_prefs = await SharedPreferences.getInstance();
|
||||
isRooted = await _rootAPI.isRooted();
|
||||
storedPatchesFile =
|
||||
(await getApplicationDocumentsDirectory()).path + storedPatchesFile;
|
||||
}
|
||||
|
||||
String getApiUrl() {
|
||||
@ -43,6 +68,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(bool bundle) {
|
||||
return _prefs.getString('patchesDownloadURL-$bundle') ?? '';
|
||||
}
|
||||
|
||||
Future<void> setPatchesDownloadURL(String value, bool bundle) async {
|
||||
await _prefs.setString('patchesDownloadURL-$bundle', value);
|
||||
}
|
||||
|
||||
String getPatchesRepo() {
|
||||
return _prefs.getString('patchesRepo') ?? defaultPatchesRepo;
|
||||
}
|
||||
@ -54,6 +98,86 @@ 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);
|
||||
}
|
||||
|
||||
String getIntegrationsRepo() {
|
||||
return _prefs.getString('integrationsRepo') ?? defaultIntegrationsRepo;
|
||||
}
|
||||
@ -81,23 +205,67 @@ class ManagerAPI {
|
||||
await _prefs.setBool('useDarkTheme', value);
|
||||
}
|
||||
|
||||
bool areUniversalPatchesEnabled() {
|
||||
return _prefs.getBool('universalPatchesEnabled') ?? false;
|
||||
}
|
||||
|
||||
Future<void> enableUniversalPatchesStatus(bool value) async {
|
||||
await _prefs.setBool('universalPatchesEnabled', value);
|
||||
}
|
||||
|
||||
bool areExperimentalPatchesEnabled() {
|
||||
return _prefs.getBool('experimentalPatchesEnabled') ?? false;
|
||||
}
|
||||
|
||||
Future<void> enableExperimentalPatchesStatus(bool value) async {
|
||||
await _prefs.setBool('experimentalPatchesEnabled', 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 +279,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 +300,92 @@ class ManagerAPI {
|
||||
}
|
||||
|
||||
Future<List<Patch>> getPatches() async {
|
||||
String repoName = getPatchesRepo();
|
||||
if (repoName == defaultPatchesRepo) {
|
||||
return await _revancedAPI.getPatches();
|
||||
} else {
|
||||
return await _githubAPI.getPatches(repoName);
|
||||
try {
|
||||
final String repoName = getPatchesRepo();
|
||||
final String currentVersion = await getCurrentPatchesVersion();
|
||||
final String url = getPatchesDownloadURL(false);
|
||||
return await _githubAPI.getPatches(
|
||||
repoName,
|
||||
currentVersion,
|
||||
url,
|
||||
);
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
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(true);
|
||||
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 +395,88 @@ 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('', false);
|
||||
await setPatchesDownloadURL('', true);
|
||||
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);
|
||||
}
|
||||
@ -198,13 +487,13 @@ class ManagerAPI {
|
||||
Future<List<PatchedApplication>> getUnsavedApps(
|
||||
List<PatchedApplication> patchedApps,
|
||||
) async {
|
||||
List<PatchedApplication> unsavedApps = [];
|
||||
bool hasRootPermissions = await _rootAPI.hasRootPermissions();
|
||||
final List<PatchedApplication> unsavedApps = [];
|
||||
final bool hasRootPermissions = await _rootAPI.hasRootPermissions();
|
||||
if (hasRootPermissions) {
|
||||
List<String> installedApps = await _rootAPI.getInstalledApps();
|
||||
for (String packageName in installedApps) {
|
||||
final List<String> installedApps = await _rootAPI.getInstalledApps();
|
||||
for (final String packageName in installedApps) {
|
||||
if (!patchedApps.any((app) => app.packageName == packageName)) {
|
||||
ApplicationWithIcon? application = await DeviceApps.getApp(
|
||||
final ApplicationWithIcon? application = await DeviceApps.getApp(
|
||||
packageName,
|
||||
true,
|
||||
) as ApplicationWithIcon?;
|
||||
@ -213,6 +502,7 @@ class ManagerAPI {
|
||||
PatchedApplication(
|
||||
name: application.appName,
|
||||
packageName: application.packageName,
|
||||
originalPackageName: application.packageName,
|
||||
version: application.versionName!,
|
||||
apkFilePath: application.apkFilePath,
|
||||
icon: application.icon,
|
||||
@ -224,15 +514,13 @@ class ManagerAPI {
|
||||
}
|
||||
}
|
||||
}
|
||||
List<Application> userApps = await DeviceApps.getInstalledApplications(
|
||||
includeSystemApps: false,
|
||||
includeAppIcons: false,
|
||||
);
|
||||
for (Application app in userApps) {
|
||||
final List<Application> userApps =
|
||||
await DeviceApps.getInstalledApplications();
|
||||
for (final 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(
|
||||
final ApplicationWithIcon? application = await DeviceApps.getApp(
|
||||
app.packageName,
|
||||
true,
|
||||
) as ApplicationWithIcon?;
|
||||
@ -241,11 +529,11 @@ class ManagerAPI {
|
||||
PatchedApplication(
|
||||
name: application.appName,
|
||||
packageName: application.packageName,
|
||||
originalPackageName: application.packageName,
|
||||
version: application.versionName!,
|
||||
apkFilePath: application.apkFilePath,
|
||||
icon: application.icon,
|
||||
patchDate: DateTime.now(),
|
||||
isRooted: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -254,24 +542,87 @@ class ManagerAPI {
|
||||
return unsavedApps;
|
||||
}
|
||||
|
||||
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);
|
||||
final List<PatchedApplication> patchedApps = getPatchedApps();
|
||||
final List<PatchedApplication> unsavedApps =
|
||||
await getUnsavedApps(patchedApps);
|
||||
patchedApps.addAll(unsavedApps);
|
||||
List<PatchedApplication> toRemove = await getAppsToRemove(patchedApps);
|
||||
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);
|
||||
for (final PatchedApplication app in patchedApps) {
|
||||
app.hasUpdates =
|
||||
await hasAppUpdates(app.originalPackageName, app.patchDate);
|
||||
app.changelog =
|
||||
await getAppChangelog(app.originalPackageName, app.patchDate);
|
||||
if (!app.hasUpdates) {
|
||||
String? currentInstalledVersion =
|
||||
final 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]'), ''));
|
||||
final String currentSavedVersion = app.version;
|
||||
final int currentInstalledVersionInt = int.parse(
|
||||
currentInstalledVersion.replaceAll(RegExp('[^0-9]'), ''),
|
||||
);
|
||||
final int currentSavedVersionInt = int.parse(
|
||||
currentSavedVersion.replaceAll(RegExp('[^0-9]'), ''),
|
||||
);
|
||||
if (currentInstalledVersionInt > currentSavedVersionInt) {
|
||||
app.hasUpdates = true;
|
||||
}
|
||||
@ -283,9 +634,9 @@ class ManagerAPI {
|
||||
|
||||
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,8 +645,11 @@ class ManagerAPI {
|
||||
return !existsNonRoot;
|
||||
}
|
||||
|
||||
Future<bool> hasAppUpdates(String packageName, DateTime patchDate) async {
|
||||
List<String> commits = await _githubAPI.getCommits(
|
||||
Future<bool> hasAppUpdates(
|
||||
String packageName,
|
||||
DateTime patchDate,
|
||||
) async {
|
||||
final List<String> commits = await _githubAPI.getCommits(
|
||||
packageName,
|
||||
getPatchesRepo(),
|
||||
patchDate,
|
||||
@ -304,7 +658,9 @@ class ManagerAPI {
|
||||
}
|
||||
|
||||
Future<List<String>> getAppChangelog(
|
||||
String packageName, DateTime patchDate) async {
|
||||
String packageName,
|
||||
DateTime patchDate,
|
||||
) async {
|
||||
List<String> newCommits = await _githubAPI.getCommits(
|
||||
packageName,
|
||||
getPatchesRepo(),
|
||||
@ -314,9 +670,79 @@ class ManagerAPI {
|
||||
newCommits = await _githubAPI.getCommits(
|
||||
packageName,
|
||||
getPatchesRepo(),
|
||||
DateTime(2022, 3, 20, 21, 06, 01),
|
||||
patchDate,
|
||||
);
|
||||
}
|
||||
return newCommits;
|
||||
}
|
||||
|
||||
Future<bool> isSplitApk(PatchedApplication patchedApp) async {
|
||||
Application? app;
|
||||
if (patchedApp.isFromStorage) {
|
||||
app = await DeviceApps.getAppFromStorage(patchedApp.apkFilePath);
|
||||
} else {
|
||||
app = await DeviceApps.getApp(patchedApp.packageName);
|
||||
}
|
||||
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 (areExperimentalPatchesEnabled() == false) {
|
||||
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);
|
||||
}
|
||||
|
||||
Future<void> resetLastSelectedPatches() async {
|
||||
final File selectedPatchesFile = File(storedPatchesFile);
|
||||
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';
|
||||
@ -18,16 +21,21 @@ class PatcherAPI {
|
||||
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 = [];
|
||||
Map filteredPatches = <String, List<Patch>>{};
|
||||
File? _outFile;
|
||||
|
||||
Future<void> initialize() async {
|
||||
await _loadPatches();
|
||||
Directory appCache = await getTemporaryDirectory();
|
||||
await _managerAPI.downloadPatches();
|
||||
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();
|
||||
}
|
||||
|
||||
@ -42,18 +50,43 @@ class PatcherAPI {
|
||||
if (_patches.isEmpty) {
|
||||
_patches = await _managerAPI.getPatches();
|
||||
}
|
||||
} on Exception {
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
_patches = List.empty();
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<ApplicationWithIcon>> getFilteredInstalledApps() async {
|
||||
List<ApplicationWithIcon> filteredApps = [];
|
||||
for (Patch patch in _patches) {
|
||||
for (Package package in patch.compatiblePackages) {
|
||||
Future<List<ApplicationWithIcon>> getFilteredInstalledApps(
|
||||
bool showUniversalPatches,
|
||||
) async {
|
||||
final List<ApplicationWithIcon> filteredApps = [];
|
||||
final bool allAppsIncluded =
|
||||
_patches.any((patch) => patch.compatiblePackages.isEmpty) &&
|
||||
showUniversalPatches;
|
||||
if (allAppsIncluded) {
|
||||
final allPackages = await DeviceApps.getInstalledApplications(
|
||||
includeAppIcons: true,
|
||||
onlyAppsWithLaunchIntent: true,
|
||||
);
|
||||
for (final pkg in allPackages) {
|
||||
if (!filteredApps.any((app) => app.packageName == pkg.packageName)) {
|
||||
final appInfo = await DeviceApps.getApp(
|
||||
pkg.packageName,
|
||||
true,
|
||||
) as ApplicationWithIcon?;
|
||||
if (appInfo != null) {
|
||||
filteredApps.add(appInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (final Patch patch in _patches) {
|
||||
for (final Package package in patch.compatiblePackages) {
|
||||
try {
|
||||
if (!filteredApps.any((app) => app.packageName == package.name)) {
|
||||
ApplicationWithIcon? app = await DeviceApps.getApp(
|
||||
final ApplicationWithIcon? app = await DeviceApps.getApp(
|
||||
package.name,
|
||||
true,
|
||||
) as ApplicationWithIcon?;
|
||||
@ -61,41 +94,47 @@ class PatcherAPI {
|
||||
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) {
|
||||
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();
|
||||
}
|
||||
|
||||
Future<bool> needsIntegrations(List<Patch> selectedPatches) async {
|
||||
return selectedPatches.any(
|
||||
(patch) => patch.dependencies.any(
|
||||
(dep) => dep.contains('integrations'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> needsResourcePatching(List<Patch> selectedPatches) async {
|
||||
Future<bool> needsResourcePatching(
|
||||
List<Patch> selectedPatches,
|
||||
) async {
|
||||
return selectedPatches.any(
|
||||
(patch) => patch.dependencies.any(
|
||||
(dep) => dep.contains('resource-'),
|
||||
@ -111,31 +150,15 @@ class PatcherAPI {
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
String apkFilePath,
|
||||
List<Patch> selectedPatches,
|
||||
) async {
|
||||
bool mergeIntegrations = await needsIntegrations(selectedPatches);
|
||||
bool resourcePatching = await needsResourcePatching(selectedPatches);
|
||||
bool includeSettings = await needsSettingsPatch(selectedPatches);
|
||||
final bool includeSettings = await needsSettingsPatch(selectedPatches);
|
||||
if (includeSettings) {
|
||||
try {
|
||||
Patch? settingsPatch = _patches.firstWhereOrNull(
|
||||
final Patch? settingsPatch = _patches.firstWhereOrNull(
|
||||
(patch) =>
|
||||
patch.name.contains('settings') &&
|
||||
patch.compatiblePackages.any((pack) => pack.name == packageName),
|
||||
@ -143,42 +166,55 @@ class PatcherAPI {
|
||||
if (settingsPatch != null) {
|
||||
selectedPatches.add(settingsPatch);
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
File? patchBundleFile = await _managerAPI.downloadPatches();
|
||||
File? integrationsFile;
|
||||
if (mergeIntegrations) {
|
||||
integrationsFile = await _managerAPI.downloadIntegrations();
|
||||
}
|
||||
final File? patchBundleFile = await _managerAPI.downloadPatches();
|
||||
final File? integrationsFile = await _managerAPI.downloadIntegrations();
|
||||
if (patchBundleFile != null) {
|
||||
_dataDir.createSync();
|
||||
_tmpDir.createSync();
|
||||
Directory workDir = _tmpDir.createTempSync('tmp-');
|
||||
File inputFile = File('${workDir.path}/base.apk');
|
||||
File patchedFile = File('${workDir.path}/patched.apk');
|
||||
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');
|
||||
Directory cacheDir = Directory('${workDir.path}/cache');
|
||||
final 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,
|
||||
'resourcePatching': resourcePatching,
|
||||
'keyStoreFilePath': _keyStoreFile.path,
|
||||
},
|
||||
);
|
||||
final String originalFilePath = apkFilePath;
|
||||
try {
|
||||
await patcherChannel.invokeMethod(
|
||||
'runPatcher',
|
||||
{
|
||||
'patchBundleFilePath': patchBundleFile.path,
|
||||
'originalFilePath': originalFilePath,
|
||||
'inputFilePath': inputFile.path,
|
||||
'patchedFilePath': patchedFile.path,
|
||||
'outFilePath': _outFile!.path,
|
||||
'integrationsPath': integrationsFile!.path,
|
||||
'selectedPatches': selectedPatches.map((p) => p.name).toList(),
|
||||
'cacheDirPath': cacheDir.path,
|
||||
'keyStoreFilePath': _keyStoreFile.path,
|
||||
'keystorePassword': _managerAPI.getKeystorePassword(),
|
||||
},
|
||||
);
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> stopPatcher() async {
|
||||
try {
|
||||
await patcherChannel.invokeMethod('stopPatcher');
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -186,7 +222,7 @@ class PatcherAPI {
|
||||
if (_outFile != null) {
|
||||
try {
|
||||
if (patchedApp.isRooted) {
|
||||
bool hasRootPermissions = await _rootAPI.hasRootPermissions();
|
||||
final bool hasRootPermissions = await _rootAPI.hasRootPermissions();
|
||||
if (hasRootPermissions) {
|
||||
return _rootAPI.installApp(
|
||||
patchedApp.packageName,
|
||||
@ -195,50 +231,89 @@ class PatcherAPI {
|
||||
);
|
||||
}
|
||||
} else {
|
||||
await AppInstaller.installApk(_outFile!.path);
|
||||
return await DeviceApps.isAppInstalled(patchedApp.packageName);
|
||||
final install = await InstallPlugin.installApk(_outFile!.path);
|
||||
return install['isSuccess'];
|
||||
}
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
} on Exception {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
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');
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> sharePatcherLog(String logs) async {
|
||||
Directory appCache = await getTemporaryDirectory();
|
||||
Directory logDir = Directory('${appCache.path}/logs');
|
||||
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();
|
||||
String dateTime = DateTime.now()
|
||||
final String dateTime = DateTime.now()
|
||||
.toIso8601String()
|
||||
.replaceAll('-', '')
|
||||
.replaceAll(':', '')
|
||||
.replaceAll('T', '')
|
||||
.replaceAll('.', '');
|
||||
File log = File('${logDir.path}/revanced-manager_patcher_$dateTime.log');
|
||||
final String fileName = 'revanced-manager_patcher_$dateTime.log';
|
||||
final File log = File('${logDir.path}/$fileName');
|
||||
log.writeAsStringSync(logs);
|
||||
ShareExtend.share(log.path, 'file');
|
||||
CRFileSaver.saveFileWithDialog(
|
||||
SaveFileDialogParams(
|
||||
sourceFilePath: log.path,
|
||||
destinationFileName: fileName,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String getRecommendedVersion(String packageName) {
|
||||
Map<String, int> versions = {};
|
||||
for (Patch patch in _patches) {
|
||||
Package? package = patch.compatiblePackages.firstWhereOrNull(
|
||||
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 (String version in package.versions) {
|
||||
for (final String version in package.versions) {
|
||||
versions.update(
|
||||
version,
|
||||
(value) => versions[version]! + 1,
|
||||
@ -248,7 +323,7 @@ class PatcherAPI {
|
||||
}
|
||||
}
|
||||
if (versions.isNotEmpty) {
|
||||
var entries = versions.entries.toList()
|
||||
final entries = versions.entries.toList()
|
||||
..sort((a, b) => a.value.compareTo(b.value));
|
||||
versions
|
||||
..clear()
|
||||
|
@ -1,68 +1,84 @@
|
||||
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:dio_cache_interceptor/dio_cache_interceptor.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:timeago/timeago.dart';
|
||||
|
||||
@lazySingleton
|
||||
class RevancedAPI {
|
||||
late Dio _dio = Dio();
|
||||
final DioCacheManager _dioCacheManager = DioCacheManager(CacheConfig());
|
||||
final Options _cacheOptions = buildCacheOptions(
|
||||
const Duration(days: 1),
|
||||
maxStale: const Duration(days: 7),
|
||||
|
||||
final _cacheOptions = CacheOptions(
|
||||
store: MemCacheStore(),
|
||||
maxStale: const Duration(days: 1),
|
||||
priority: CachePriority.high,
|
||||
);
|
||||
|
||||
Future<void> initialize(String apiUrl) async {
|
||||
_dio = Dio(BaseOptions(baseUrl: apiUrl));
|
||||
_dio.interceptors.add(_dioCacheManager.interceptor);
|
||||
try {
|
||||
_dio = Dio(
|
||||
BaseOptions(
|
||||
baseUrl: apiUrl,
|
||||
),
|
||||
);
|
||||
|
||||
_dio.interceptors.add(DioCacheInterceptor(options: _cacheOptions));
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> clearAllCache() async {
|
||||
await _dioCacheManager.clearAll();
|
||||
try {
|
||||
await _cacheOptions.store!.clean();
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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'];
|
||||
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 {
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -72,49 +88,101 @@ 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'];
|
||||
final String url = release['browser_download_url'];
|
||||
return await DefaultCacheManager().getSingleFile(url);
|
||||
}
|
||||
} on Exception {
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
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 DefaultCacheManager().getFileStream(
|
||||
release!['browser_download_url'] as String,
|
||||
withProgress: true,
|
||||
)) {
|
||||
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,10 +1,25 @@
|
||||
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 {
|
||||
final bool? isRooted = await Root.isRootAvailable();
|
||||
return isRooted != null && isRooted;
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> hasRootPermissions() async {
|
||||
try {
|
||||
bool? isRooted = await Root.isRootAvailable();
|
||||
@ -13,7 +28,10 @@ class RootAPI {
|
||||
return isRooted != null && isRooted;
|
||||
}
|
||||
return false;
|
||||
} on Exception {
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -24,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 {
|
||||
@ -79,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"',
|
||||
@ -98,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);
|
||||
@ -112,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"',
|
||||
);
|
||||
@ -131,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"',
|
||||
);
|
||||
@ -142,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"',
|
||||
);
|
||||
@ -155,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"',
|
||||
);
|
||||
@ -168,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"',
|
||||
@ -215,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),
|
||||
);
|
||||
|
@ -7,65 +7,44 @@ 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;
|
||||
|
||||
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
|
||||
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(
|
||||
|
@ -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,7 +20,7 @@ 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,
|
||||
@ -36,20 +37,19 @@ 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,
|
||||
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(),
|
||||
),
|
||||
@ -61,7 +61,6 @@ class _AppSelectorViewState extends State<AppSelectorView> {
|
||||
horizontal: 12.0,
|
||||
),
|
||||
child: SearchBar(
|
||||
showSelectIcon: false,
|
||||
hintText: FlutterI18n.translate(
|
||||
context,
|
||||
'appSelectorView.searchBarHint',
|
||||
@ -78,26 +77,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,49 +1,196 @@
|
||||
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: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,
|
||||
originalPackageName: application.packageName,
|
||||
version: application.versionName!,
|
||||
apkFilePath: application.apkFilePath,
|
||||
icon: application.icon,
|
||||
patchDate: DateTime.now(),
|
||||
);
|
||||
locator<PatcherViewModel>().selectedPatches.clear();
|
||||
locator<PatcherViewModel>().notifyListeners();
|
||||
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) {
|
||||
return showSelectFromStorageDialog(context);
|
||||
} else if (!await checkSplitApk(packageName) || isRooted) {
|
||||
selectApp(app);
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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?;
|
||||
@ -51,27 +198,46 @@ class AppSelectorViewModel extends BaseViewModel {
|
||||
locator<PatcherViewModel>().selectedApp = PatchedApplication(
|
||||
name: application.appName,
|
||||
packageName: application.packageName,
|
||||
originalPackageName: application.packageName,
|
||||
version: application.versionName!,
|
||||
apkFilePath: result.files.single.path!,
|
||||
icon: application.icon,
|
||||
patchDate: DateTime.now(),
|
||||
isFromStorage: true,
|
||||
);
|
||||
locator<PatcherViewModel>().selectedPatches.clear();
|
||||
locator<PatcherViewModel>().notifyListeners();
|
||||
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()),
|
||||
)
|
||||
.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,52 +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/installerView/custom_material_button.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');
|
||||
}
|
||||
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');
|
||||
}
|
||||
}
|
||||
_getPatchedApps();
|
||||
_managerAPI.reAssessSavedApps().then((_) => _getPatchedApps());
|
||||
@ -64,7 +98,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,10 +107,7 @@ class HomeViewModel extends BaseViewModel {
|
||||
}
|
||||
|
||||
void _getPatchedApps() {
|
||||
patchedInstalledApps = _managerAPI
|
||||
.getPatchedApps()
|
||||
.where((app) => app.hasUpdates == false)
|
||||
.toList();
|
||||
patchedInstalledApps = _managerAPI.getPatchedApps().toList();
|
||||
patchedUpdatableApps = _managerAPI
|
||||
.getPatchedApps()
|
||||
.where((app) => app.hasUpdates == true)
|
||||
@ -85,100 +116,365 @@ class HomeViewModel extends BaseViewModel {
|
||||
}
|
||||
|
||||
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) {
|
||||
_lastUpdate!.difference(DateTime.now()).inSeconds > 2) {
|
||||
_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/installerView/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,133 +13,90 @@ 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: Scaffold(
|
||||
body: CustomScrollView(
|
||||
controller: model.scrollController,
|
||||
slivers: <Widget>[
|
||||
CustomSliverAppBar(
|
||||
title: Text(
|
||||
model.headerLogs,
|
||||
style: GoogleFonts.inter(
|
||||
color: Theme.of(context).textTheme.headline6!.color,
|
||||
),
|
||||
),
|
||||
onBackButtonPressed: () => model.onWillPop(context),
|
||||
actions: <Widget>[
|
||||
Visibility(
|
||||
visible: !model.isPatching && !model.hasErrors,
|
||||
child: CustomPopupMenu(
|
||||
onSelected: (value) => model.onMenuSelection(value),
|
||||
children: {
|
||||
0: I18nText(
|
||||
'installerView.shareApkMenuOption',
|
||||
child: const Text(
|
||||
'',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
bottom: false,
|
||||
child: Scaffold(
|
||||
floatingActionButton: Visibility(
|
||||
visible: !model.isPatching,
|
||||
child: FloatingActionButton.extended(
|
||||
label: I18nText('installerView.installButton'),
|
||||
icon: const Icon(Icons.file_download_outlined),
|
||||
onPressed: () => 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',
|
||||
),
|
||||
1: I18nText(
|
||||
'installerView.shareLogMenuOption',
|
||||
child: const Text(
|
||||
'',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
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>[
|
||||
CustomSliverAppBar(
|
||||
title: Text(
|
||||
model.headerLogs,
|
||||
style: GoogleFonts.inter(
|
||||
color: Theme.of(context).textTheme.titleLarge!.color,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
bottom: PreferredSize(
|
||||
onBackButtonPressed: () => model.onWillPop(context),
|
||||
bottom: PreferredSize(
|
||||
preferredSize: const Size(double.infinity, 1.0),
|
||||
child:
|
||||
GradientProgressIndicator(progress: model.progress!)),
|
||||
),
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
sliver: SliverList(
|
||||
delegate: SliverChildListDelegate.fixed(
|
||||
<Widget>[
|
||||
CustomCard(
|
||||
child: Text(
|
||||
model.logs,
|
||||
style: GoogleFonts.jetBrainsMono(
|
||||
fontSize: 13,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
child: GradientProgressIndicator(progress: model.progress),
|
||||
),
|
||||
),
|
||||
),
|
||||
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();
|
||||
},
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
sliver: SliverList(
|
||||
delegate: SliverChildListDelegate.fixed(
|
||||
<Widget>[
|
||||
CustomCard(
|
||||
child: Text(
|
||||
model.logs,
|
||||
style: GoogleFonts.jetBrainsMono(
|
||||
fontSize: 13,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
Visibility(
|
||||
visible: !model.isInstalled,
|
||||
child: CustomMaterialButton(
|
||||
isFilled: false,
|
||||
label:
|
||||
I18nText('installerView.installRootButton'),
|
||||
isExpanded: true,
|
||||
onPressed: () => model.installResult(
|
||||
context,
|
||||
true,
|
||||
),
|
||||
),
|
||||
),
|
||||
Visibility(
|
||||
visible: !model.isInstalled,
|
||||
child: const SizedBox(
|
||||
width: 16,
|
||||
),
|
||||
),
|
||||
Visibility(
|
||||
visible: !model.isInstalled,
|
||||
child: CustomMaterialButton(
|
||||
label: I18nText('installerView.installButton'),
|
||||
isExpanded: true,
|
||||
onPressed: () => model.installResult(
|
||||
context,
|
||||
false,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
onWillPop: () => model.onWillPop(context),
|
||||
|
@ -1,4 +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';
|
||||
@ -9,15 +11,17 @@ 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/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/installerView/custom_material_button.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
import 'package:wakelock/wakelock.dart';
|
||||
|
||||
class InstallerViewModel extends BaseViewModel {
|
||||
final ManagerAPI _managerAPI = locator<ManagerAPI>();
|
||||
final PatcherAPI _patcherAPI = locator<PatcherAPI>();
|
||||
final RootAPI _rootAPI = RootAPI();
|
||||
final Toast _toast = locator<Toast>();
|
||||
final PatchedApplication _app = locator<PatcherViewModel>().selectedApp!;
|
||||
final List<Patch> _patches = locator<PatcherViewModel>().selectedPatches;
|
||||
@ -28,11 +32,15 @@ class InstallerViewModel extends BaseViewModel {
|
||||
double? progress = 0.0;
|
||||
String logs = '';
|
||||
String headerLogs = '';
|
||||
bool isRooted = false;
|
||||
bool isPatching = true;
|
||||
bool isInstalled = false;
|
||||
bool hasErrors = false;
|
||||
bool isCanceled = false;
|
||||
bool cancel = false;
|
||||
|
||||
Future<void> initialize(BuildContext context) async {
|
||||
isRooted = await _rootAPI.isRooted();
|
||||
if (await Permission.ignoreBatteryOptimizations.isGranted) {
|
||||
try {
|
||||
FlutterBackground.initialize(
|
||||
@ -45,15 +53,15 @@ 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
|
||||
}
|
||||
}
|
||||
await Wakelock.enable();
|
||||
@ -66,10 +74,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;
|
||||
@ -77,7 +85,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;
|
||||
}
|
||||
@ -89,6 +97,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;
|
||||
@ -116,125 +129,262 @@ 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',
|
||||
);
|
||||
try {
|
||||
update(0.0, 'Initializing...', 'Initializing installer');
|
||||
if (_patches.isNotEmpty) {
|
||||
try {
|
||||
update(0.1, '', 'Creating working directory');
|
||||
await _patcherAPI.runPatcher(
|
||||
_app.packageName,
|
||||
_app.apkFilePath,
|
||||
_patches,
|
||||
);
|
||||
} on Exception catch (e) {
|
||||
update(
|
||||
-100.0,
|
||||
'Aborted...',
|
||||
'An error occurred! Aborted\nError:\n$e',
|
||||
);
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
update(-100.0, 'Aborted...', 'No app or patches selected! Aborted');
|
||||
}
|
||||
} else {
|
||||
update(-100.0, 'Aborting...', 'No app or patches selected! Aborting');
|
||||
}
|
||||
if (FlutterBackground.isBackgroundExecutionEnabled) {
|
||||
try {
|
||||
FlutterBackground.disableBackgroundExecution();
|
||||
} on Exception {
|
||||
// ignore
|
||||
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(
|
||||
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.installErrorDialogTitle'),
|
||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
content: I18nText(
|
||||
rootMicroG
|
||||
? 'installerView.installErrorDialogText1'
|
||||
: rootFromStorage
|
||||
? 'installerView.installErrorDialogText3'
|
||||
: 'installerView.installErrorDialogText2',
|
||||
title: I18nText(
|
||||
'installerView.installType',
|
||||
),
|
||||
actions: <Widget>[
|
||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
icon: const Icon(Icons.file_download_outlined),
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 16),
|
||||
content: 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'),
|
||||
subtitle: I18nText('installerView.installRecommendedType'),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
value: 0,
|
||||
groupValue: value,
|
||||
onChanged: (selected) {
|
||||
installType.value = selected!;
|
||||
},
|
||||
),
|
||||
RadioListTile(
|
||||
title: I18nText('installerView.installRootType'),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
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 {
|
||||
update(
|
||||
1.0,
|
||||
'Installing...',
|
||||
_app.isRooted
|
||||
? 'Installing patched file using root method'
|
||||
: 'Installing patched file using nonroot method',
|
||||
);
|
||||
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.',
|
||||
);
|
||||
}
|
||||
await _managerAPI.savePatchedApp(_app);
|
||||
installResult(context, false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> stopPatcher() async {
|
||||
try {
|
||||
isCanceled = true;
|
||||
update(0.5, 'Aborting...', 'Canceling patching process');
|
||||
await _patcherAPI.stopPatcher();
|
||||
update(-100.0, 'Aborted...', 'Press back to exit');
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void shareResult() {
|
||||
_patcherAPI.sharePatchedFile(_app.name, _app.version);
|
||||
Future<void> installResult(BuildContext context, bool installAsRoot) async {
|
||||
try {
|
||||
_app.isRooted = installAsRoot;
|
||||
final bool hasMicroG =
|
||||
_patches.any((p) => p.name.endsWith('MicroG support'));
|
||||
final bool rootMicroG = installAsRoot && hasMicroG;
|
||||
final bool rootFromStorage = installAsRoot && _app.isFromStorage;
|
||||
final 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',
|
||||
),
|
||||
actions: <Widget>[
|
||||
CustomMaterialButton(
|
||||
label: I18nText('okButton'),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
update(
|
||||
1.0,
|
||||
'Installing...',
|
||||
_app.isRooted
|
||||
? 'Installing patched file using root method'
|
||||
: 'Installing patched file using nonroot method',
|
||||
);
|
||||
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.',
|
||||
);
|
||||
}
|
||||
await _managerAPI.savePatchedApp(_app);
|
||||
}
|
||||
}
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void shareLog() {
|
||||
_patcherAPI.sharePatcherLog(logs);
|
||||
void exportResult() {
|
||||
try {
|
||||
_patcherAPI.exportPatchedFile(_app.name, _app.version);
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void exportLog() {
|
||||
_patcherAPI.exportPatcherLog(logs);
|
||||
}
|
||||
|
||||
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();
|
||||
exportLog();
|
||||
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();
|
||||
}
|
||||
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: '',
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -15,10 +15,14 @@ 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();
|
||||
requestManageExternalStorage();
|
||||
|
||||
if (prefs.getBool('permissionsRequested') == null) {
|
||||
await Permission.storage.request();
|
||||
await Permission.manageExternalStorage.request();
|
||||
await prefs.setBool('permissionsRequested', true);
|
||||
RootAPI().hasRootPermissions().then(
|
||||
(value) => Permission.requestInstallPackages.request().then(
|
||||
@ -26,9 +30,10 @@ class NavigationViewModel extends IndexTrackingViewModel {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (prefs.getBool('useDarkTheme') == null) {
|
||||
bool isDark =
|
||||
MediaQuery.of(context).platformBrightness != Brightness.light;
|
||||
final bool isDark =
|
||||
MediaQuery.platformBrightnessOf(context) != Brightness.light;
|
||||
await prefs.setBool('useDarkTheme', isDark);
|
||||
await DynamicTheme.of(context)!.setTheme(isDark ? 1 : 0);
|
||||
}
|
||||
@ -44,6 +49,17 @@ class NavigationViewModel extends IndexTrackingViewModel {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> requestManageExternalStorage() async {
|
||||
final manageExternalStorageStatus =
|
||||
await Permission.manageExternalStorage.status;
|
||||
if (manageExternalStorageStatus.isDenied) {
|
||||
await Permission.manageExternalStorage.request();
|
||||
}
|
||||
if (manageExternalStorageStatus.isPermanentlyDenied) {
|
||||
await openAppSettings();
|
||||
}
|
||||
}
|
||||
|
||||
Widget getViewForIndex(int index) {
|
||||
switch (index) {
|
||||
case 0:
|
||||
|
@ -22,7 +22,7 @@ class PatcherView extends StatelessWidget {
|
||||
child: FloatingActionButton.extended(
|
||||
label: I18nText('patcherView.patchButton'),
|
||||
icon: const Icon(Icons.build),
|
||||
onPressed: () => model.showPatchConfirmationDialog(context),
|
||||
onPressed: () => model.showRemovedPatchesDialog(context),
|
||||
),
|
||||
),
|
||||
body: CustomScrollView(
|
||||
@ -34,7 +34,7 @@ class PatcherView extends StatelessWidget {
|
||||
child: Text(
|
||||
'',
|
||||
style: GoogleFonts.inter(
|
||||
color: Theme.of(context).textTheme.headline6!.color,
|
||||
color: Theme.of(context).textTheme.titleLarge!.color,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'package:device_apps/device_apps.dart';
|
||||
// ignore_for_file: use_build_context_synchronously
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_i18n/flutter_i18n.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
@ -6,17 +7,22 @@ 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/ui/widgets/installerView/custom_material_button.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';
|
||||
|
||||
@lazySingleton
|
||||
class PatcherViewModel extends BaseViewModel {
|
||||
final NavigationService _navigationService = locator<NavigationService>();
|
||||
final ManagerAPI _managerAPI = locator<ManagerAPI>();
|
||||
final PatcherAPI _patcherAPI = locator<PatcherAPI>();
|
||||
PatchedApplication? selectedApp;
|
||||
List<Patch> selectedPatches = [];
|
||||
List<String> removedPatches = [];
|
||||
|
||||
void navigateToAppSelector() {
|
||||
_navigationService.navigateTo(Routes.appSelectorView);
|
||||
@ -39,28 +45,59 @@ class PatcherViewModel extends BaseViewModel {
|
||||
}
|
||||
|
||||
Future<bool> isValidPatchConfig() async {
|
||||
bool needsResourcePatching =
|
||||
await _patcherAPI.needsResourcePatching(selectedPatches);
|
||||
final bool needsResourcePatching = await _patcherAPI.needsResourcePatching(
|
||||
selectedPatches,
|
||||
);
|
||||
if (needsResourcePatching && selectedApp != null) {
|
||||
Application? app = await DeviceApps.getApp(selectedApp!.packageName);
|
||||
if (app != null && app.isSplit) {
|
||||
return false;
|
||||
}
|
||||
final bool isSplit = await _managerAPI.isSplitApk(selectedApp!);
|
||||
return !isSplit;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<void> showPatchConfirmationDialog(BuildContext context) async {
|
||||
bool isValid = await isValidPatchConfig();
|
||||
if (isValid) {
|
||||
navigateToInstaller();
|
||||
} else {
|
||||
final bool isValid = await isValidPatchConfig();
|
||||
if (context.mounted) {
|
||||
if (isValid) {
|
||||
showArmv7WarningDialog(context);
|
||||
} else {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: I18nText('warning'),
|
||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
content: I18nText('patcherView.splitApkWarningDialogText'),
|
||||
actions: <Widget>[
|
||||
CustomMaterialButton(
|
||||
label: I18nText('noButton'),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
CustomMaterialButton(
|
||||
label: I18nText('yesButton'),
|
||||
isFilled: false,
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
showArmv7WarningDialog(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> showRemovedPatchesDialog(BuildContext context) async {
|
||||
if (removedPatches.isNotEmpty) {
|
||||
return 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,
|
||||
@ -73,10 +110,46 @@ class PatcherViewModel extends BaseViewModel {
|
||||
Navigator.of(context).pop();
|
||||
navigateToInstaller();
|
||||
},
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
showArmv7WarningDialog(context);
|
||||
}
|
||||
}
|
||||
|
||||
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 +161,54 @@ 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!.originalPackageName);
|
||||
final List<Patch> patches =
|
||||
_patcherAPI.getFilteredPatches(selectedApp!.originalPackageName);
|
||||
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.areExperimentalPatchesEnabled()) {
|
||||
this.selectedPatches.removeWhere((patch) => !isPatchSupported(patch));
|
||||
}
|
||||
if (!_managerAPI.areUniversalPatchesEnabled()) {
|
||||
this
|
||||
.selectedPatches
|
||||
.removeWhere((patch) => patch.compatiblePackages.isEmpty);
|
||||
}
|
||||
final usedPatches = _managerAPI.getUsedPatches(selectedApp!.originalPackageName);
|
||||
for (final patch in usedPatches){
|
||||
if (!patches.any((p) => p.name == patch.name)){
|
||||
removedPatches.add('\u2022 ${patch.name}');
|
||||
}
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,12 @@
|
||||
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/custom_popup_menu.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,18 +18,35 @@ 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();
|
||||
@ -38,23 +59,60 @@ 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,
|
||||
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(),
|
||||
),
|
||||
actions: [
|
||||
FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(top: 12, bottom: 12),
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 6, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.tertiary
|
||||
.withOpacity(0.5),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Text(
|
||||
model.patchesVersion!,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).textTheme.titleLarge!.color,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
CustomPopupMenu(
|
||||
onSelected: (value) =>
|
||||
{model.onMenuSelection(value, context)},
|
||||
children: {
|
||||
0: I18nText(
|
||||
'patchesSelectorView.loadPatchesSelection',
|
||||
child: const Text(
|
||||
'',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
bottom: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(64.0),
|
||||
child: Padding(
|
||||
@ -63,7 +121,6 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
|
||||
horizontal: 12.0,
|
||||
),
|
||||
child: SearchBar(
|
||||
showSelectIcon: true,
|
||||
hintText: FlutterI18n.translate(
|
||||
context,
|
||||
'patchesSelectorView.searchBarHint',
|
||||
@ -73,7 +130,6 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
|
||||
_query = searchQuery;
|
||||
});
|
||||
},
|
||||
onSelectAll: (value) => model.selectAllPatches(value),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -93,106 +149,110 @@ 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,
|
||||
),
|
||||
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,
|
||||
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);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
...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(),
|
||||
isNew: model.isPatchNew(
|
||||
patch,
|
||||
model.getAppInfo().packageName,
|
||||
),
|
||||
isSelected: model.isSelected(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: I18nText(
|
||||
'patchesSelectorView.universalPatches',
|
||||
),
|
||||
),
|
||||
...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(),
|
||||
isNew: false,
|
||||
isSelected: model.isSelected(patch),
|
||||
onChanged: (value) => model.selectPatch(
|
||||
patch,
|
||||
value,
|
||||
context,
|
||||
),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 8,
|
||||
horizontal: 8,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
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',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
collapsed: Container(),
|
||||
) */
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
} else {
|
||||
return Container();
|
||||
}
|
||||
}),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 70.0),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -1,22 +1,44 @@
|
||||
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/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 PatchesSelectorViewModel extends BaseViewModel {
|
||||
final PatcherAPI _patcherAPI = locator<PatcherAPI>();
|
||||
final ManagerAPI _managerAPI = locator<ManagerAPI>();
|
||||
final List<Patch> patches = [];
|
||||
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!.originalPackageName,
|
||||
),
|
||||
);
|
||||
patches.sort((a, b) {
|
||||
if (isPatchNew(a, selectedApp!.packageName) ==
|
||||
isPatchNew(b, selectedApp!.packageName)) {
|
||||
return a.name.compareTo(b.name);
|
||||
} else {
|
||||
return isPatchNew(b, selectedApp!.packageName) ? 1 : -1;
|
||||
}
|
||||
});
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@ -26,46 +48,124 @@ class PatchesSelectorViewModel extends BaseViewModel {
|
||||
);
|
||||
}
|
||||
|
||||
void selectPatch(Patch patch, bool isSelected) {
|
||||
if (isSelected && !selectedPatches.contains(patch)) {
|
||||
selectedPatches.add(patch);
|
||||
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 {
|
||||
selectedPatches.remove(patch);
|
||||
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?.originalPackageName != null) {
|
||||
selectedPatches.addAll(
|
||||
_patcherAPI
|
||||
.getFilteredPatches(
|
||||
locator<PatcherViewModel>().selectedApp!.originalPackageName,
|
||||
)
|
||||
.where(
|
||||
(element) =>
|
||||
!element.excluded &&
|
||||
(_managerAPI.areExperimentalPatchesEnabled() ||
|
||||
isPatchSupported(element)),
|
||||
),
|
||||
);
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void selectAllPatches(bool isSelected) {
|
||||
void clearPatches() {
|
||||
selectedPatches.clear();
|
||||
if (isSelected) {
|
||||
selectedPatches.addAll(patches);
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void selectPatches() {
|
||||
locator<PatcherViewModel>().selectedPatches = selectedPatches;
|
||||
saveSelectedPatches();
|
||||
locator<PatcherViewModel>().notifyListeners();
|
||||
}
|
||||
|
||||
List<Patch> getQueriedPatches(String query) {
|
||||
return patches
|
||||
.where((patch) =>
|
||||
query.isEmpty ||
|
||||
query.length < 2 ||
|
||||
patch.name.toLowerCase().contains(
|
||||
query.toLowerCase(),
|
||||
))
|
||||
.toList();
|
||||
Future<void> getPatchesVersion() async {
|
||||
patchesVersion = await _managerAPI.getCurrentPatchesVersion();
|
||||
}
|
||||
|
||||
String getAppVersion() {
|
||||
return locator<PatcherViewModel>().selectedApp!.version;
|
||||
List<Patch> getQueriedPatches(String query) {
|
||||
final List<Patch> patch = patches
|
||||
.where(
|
||||
(patch) =>
|
||||
query.isEmpty ||
|
||||
query.length < 2 ||
|
||||
patch.name.toLowerCase().contains(query.toLowerCase()) ||
|
||||
patch.getSimpleName().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, String packageName) {
|
||||
final List<Patch> savedPatches = _managerAPI.getSavedPatches(packageName);
|
||||
if (savedPatches.isEmpty) {
|
||||
return false;
|
||||
} else {
|
||||
return !savedPatches
|
||||
.any((p) => p.getSimpleName() == patch.getSimpleName());
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
@ -75,10 +175,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!.originalPackageName,
|
||||
selectedPatches,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> loadSelectedPatches(BuildContext context) async {
|
||||
if (_managerAPI.isPatchesChangeEnabled()) {
|
||||
final List<String> selectedPatches = await _managerAPI.getSelectedPatches(
|
||||
locator<PatcherViewModel>().selectedApp!.originalPackageName,
|
||||
);
|
||||
if (selectedPatches.isNotEmpty) {
|
||||
this.selectedPatches.clear();
|
||||
this.selectedPatches.addAll(
|
||||
patches.where((patch) => selectedPatches.contains(patch.name)),
|
||||
);
|
||||
if (!_managerAPI.areExperimentalPatchesEnabled()) {
|
||||
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,116 @@
|
||||
// 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/views/settings/settings_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/widgets/settingsView/settings_section.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
|
||||
final _settingViewModel = SettingsViewModel();
|
||||
|
||||
// ignore: constant_identifier_names
|
||||
const int ANDROID_12_SDK_VERSION = 31;
|
||||
|
||||
class SUpdateTheme extends BaseViewModel {
|
||||
final ManagerAPI _managerAPI = locator<ManagerAPI>();
|
||||
|
||||
bool getDynamicThemeStatus() {
|
||||
return _managerAPI.getUseDynamicTheme();
|
||||
}
|
||||
|
||||
Future<void> setUseDynamicTheme(BuildContext context, bool value) async {
|
||||
await _managerAPI.setUseDynamicTheme(value);
|
||||
final 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);
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool getDarkThemeStatus() {
|
||||
return _managerAPI.getUseDarkTheme();
|
||||
}
|
||||
|
||||
Future<void> setUseDarkTheme(BuildContext context, bool value) async {
|
||||
await _managerAPI.setUseDarkTheme(value);
|
||||
final int currentTheme = DynamicTheme.of(context)!.themeId;
|
||||
if (currentTheme < 2) {
|
||||
await DynamicTheme.of(context)!.setTheme(value ? 1 : 0);
|
||||
} else {
|
||||
await DynamicTheme.of(context)!.setTheme(value ? 3 : 2);
|
||||
}
|
||||
SystemChrome.setSystemUIOverlayStyle(
|
||||
SystemUiOverlayStyle(
|
||||
systemNavigationBarIconBrightness:
|
||||
value ? Brightness.light : Brightness.dark,
|
||||
),
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
class SUpdateThemeUI extends StatelessWidget {
|
||||
const SUpdateThemeUI({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SettingsSection(
|
||||
title: 'settingsView.appearanceSectionTitle',
|
||||
children: <Widget>[
|
||||
SwitchListTile(
|
||||
contentPadding: 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: SUpdateTheme().getDarkThemeStatus(),
|
||||
onChanged: (value) => SUpdateTheme().setUseDarkTheme(
|
||||
context,
|
||||
value,
|
||||
),
|
||||
),
|
||||
FutureBuilder<int>(
|
||||
future: _settingViewModel.getSdkVersion(),
|
||||
builder: (context, snapshot) => Visibility(
|
||||
visible:
|
||||
snapshot.hasData && snapshot.data! >= ANDROID_12_SDK_VERSION,
|
||||
child: 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: _settingViewModel.sUpdateTheme.getDynamicThemeStatus(),
|
||||
onChanged: (value) => {
|
||||
_settingViewModel.sUpdateTheme.setUseDynamicTheme(
|
||||
context,
|
||||
value,
|
||||
),
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -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_export_section.dart';
|
||||
import 'package:revanced_manager/ui/widgets/settingsView/settings_info_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,131 +40,16 @@ 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),
|
||||
),
|
||||
SUpdateThemeUI(),
|
||||
// SUpdateLanguageUI(),
|
||||
// _settingsDivider,
|
||||
STeamSection(),
|
||||
_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),
|
||||
),
|
||||
],
|
||||
),
|
||||
SAdvancedSection(),
|
||||
_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),
|
||||
),
|
||||
],
|
||||
),
|
||||
SExportSection(),
|
||||
_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),
|
||||
),
|
||||
],
|
||||
),
|
||||
SInfoSection(),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -1,335 +1,283 @@
|
||||
// ignore_for_file: use_build_context_synchronously
|
||||
import 'dart:io';
|
||||
import 'package:cr_file_saver/file_saver.dart';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:dynamic_themes/dynamic_themes.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/ui/widgets/installerView/custom_material_button.dart';
|
||||
import 'package:revanced_manager/ui/widgets/settingsView/custom_text_field.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/views/settings/settingsFragment/settings_update_theme.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.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();
|
||||
final SUpdateTheme sUpdateTheme = SUpdateTheme();
|
||||
|
||||
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('noButton'),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
CustomMaterialButton(
|
||||
label: I18nText('yesButton'),
|
||||
onPressed: () {
|
||||
_managerAPI.setChangingToggleModified(true);
|
||||
_managerAPI.setPatchesChangeEnabled(true);
|
||||
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 areExperimentalPatchesEnabled() {
|
||||
return _managerAPI.areExperimentalPatchesEnabled();
|
||||
}
|
||||
|
||||
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 useExperimentalPatches(bool value) {
|
||||
_managerAPI.enableExperimentalPatchesStatus(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<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 resetSelectedPatches() {
|
||||
_managerAPI.resetLastSelectedPatches();
|
||||
_toast.showBottom('settingsView.resetStoredPatches');
|
||||
}
|
||||
|
||||
Future<int> getSdkVersion() async {
|
||||
AndroidDeviceInfo info = await DeviceInfoPlugin().androidInfo;
|
||||
return info.version.sdkInt ?? -1;
|
||||
final AndroidDeviceInfo info = await DeviceInfoPlugin().androidInfo;
|
||||
return info.version.sdkInt;
|
||||
}
|
||||
|
||||
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,
|
||||
@ -260,6 +222,22 @@ class AppInfoView extends StatelessWidget {
|
||||
subtitle: Text(app.packageName),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
ListTile(
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
title: I18nText(
|
||||
'appInfoView.originalPackageNameLabel',
|
||||
child: const Text(
|
||||
'',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
subtitle: Text(app.originalPackageName),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
ListTile(
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
|