mirror of
https://github.com/ReVanced/revanced-manager.git
synced 2026-01-12 06:06:20 +00:00
Compare commits
328 Commits
feat/impro
...
settings-r
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1851a35e10 | ||
|
|
f4a2b06276 | ||
|
|
bbfd625ac1 | ||
|
|
c012a3e9c0 | ||
|
|
e10e5e4e3f | ||
|
|
ede1ab5ed4 | ||
|
|
1092188ab0 | ||
|
|
f348eba115 | ||
|
|
3d234820a3 | ||
|
|
cd06d36f68 | ||
|
|
242c4570ce | ||
|
|
71b73a3b42 | ||
|
|
067020f38f | ||
|
|
2aef67872d | ||
|
|
818dc09aa4 | ||
|
|
a762969966 | ||
|
|
74338931b8 | ||
|
|
0ab424bfdb | ||
|
|
fff1a41fee | ||
|
|
7644a74648 | ||
|
|
9db3bd5b3f | ||
|
|
b81bd17fbc | ||
|
|
cf3866f892 | ||
|
|
5d3a81f4b9 | ||
|
|
f9831d4da5 | ||
|
|
8a20d8cf9b | ||
|
|
49f75f9edd | ||
|
|
9916e4da4d | ||
|
|
2ec1c0238d | ||
|
|
9dc716b1c8 | ||
|
|
31fb8b1404 | ||
|
|
0685479d53 | ||
|
|
20c13ee71c | ||
|
|
cf322147d5 | ||
|
|
b4c37e6ddc | ||
|
|
697386c36c | ||
|
|
f6f72387b9 | ||
|
|
d201bdc422 | ||
|
|
2055400565 | ||
|
|
10e7e4b39f | ||
|
|
edb4e421e2 | ||
|
|
747017a5f9 | ||
|
|
e992a99783 | ||
|
|
e869db0555 | ||
|
|
936a9efd0b | ||
|
|
edc11b6e1d | ||
|
|
32f2710763 | ||
|
|
4257c32bf5 | ||
|
|
c18901c35b | ||
|
|
f126fe9fa8 | ||
|
|
4cbd480e84 | ||
|
|
afc72ffd85 | ||
|
|
cdc69ea8ff | ||
|
|
52a89b1638 | ||
|
|
641d518b6e | ||
|
|
2eca45d397 | ||
|
|
de6fddf405 | ||
|
|
45f32040f8 | ||
|
|
42ef2ca99d | ||
|
|
563f2f37f7 | ||
|
|
881b13740a | ||
|
|
d39804f7ed | ||
|
|
b8a85c4891 | ||
|
|
37e9630b9c | ||
|
|
40492b67d1 | ||
|
|
f655a6e03a | ||
|
|
36f864efbb | ||
|
|
a995f43b7b | ||
|
|
1ee1330e47 | ||
|
|
3f4a234915 | ||
|
|
032ca39cf6 | ||
|
|
6f9a984541 | ||
|
|
e6e043f168 | ||
|
|
7c7fb7b343 | ||
|
|
b26fe30861 | ||
|
|
f99cdfe926 | ||
|
|
ec0a077539 | ||
|
|
a22158d070 | ||
|
|
48fe3a707e | ||
|
|
8d3d500b7b | ||
|
|
d8248cc915 | ||
|
|
397a1f8f9c | ||
|
|
a0f187354f | ||
|
|
1bf004ddee | ||
|
|
495100dea9 | ||
|
|
e3bd8a8b22 | ||
|
|
d987ac6c7a | ||
|
|
9883dcd0a7 | ||
|
|
d63133189d | ||
|
|
39fbb87010 | ||
|
|
f197760efd | ||
|
|
fa13d4a538 | ||
|
|
71b5f539c1 | ||
|
|
603a827e45 | ||
|
|
47bdc69a43 | ||
|
|
1ce56af3b1 | ||
|
|
a12c5c583b | ||
|
|
5455cf20ab | ||
|
|
75fcdb139c | ||
|
|
269fa79136 | ||
|
|
16fea59605 | ||
|
|
c7d183ee8d | ||
|
|
413083c58d | ||
|
|
1f671aba33 | ||
|
|
6bfd9098d6 | ||
|
|
4e7d96e91d | ||
|
|
ac0a036035 | ||
|
|
2a1445d61f | ||
|
|
0df39a1136 | ||
|
|
634d793839 | ||
|
|
afd6c5d6b7 | ||
|
|
ab0682cc5c | ||
|
|
60ca901ac7 | ||
|
|
91ca5be57a | ||
|
|
ac47a7eaa4 | ||
|
|
ce134224a8 | ||
|
|
ca49d3a465 | ||
|
|
5d7f9d1387 | ||
|
|
8d5d86fea8 | ||
|
|
6709505e9e | ||
|
|
088de60c91 | ||
|
|
ef041869e5 | ||
|
|
16cdc7aca4 | ||
|
|
39536c0e18 | ||
|
|
607d8b67c9 | ||
|
|
0b9889ea44 | ||
|
|
4acef776b2 | ||
|
|
e186dfdaa9 | ||
|
|
c0f3d02e6f | ||
|
|
36c8f59d6f | ||
|
|
f38b31a591 | ||
|
|
3232bb10e6 | ||
|
|
b7cb6b94f5 | ||
|
|
aa6e612fba | ||
|
|
d9d7b98409 | ||
|
|
4fdd6bbe5f | ||
|
|
439f6250f3 | ||
|
|
d55abf5dda | ||
|
|
a8b9d9316f | ||
|
|
55c7800f39 | ||
|
|
d9eb1c42bc | ||
|
|
8cd617e32d | ||
|
|
a17a05995a | ||
|
|
de4e616dcc | ||
|
|
12b00e5c8d | ||
|
|
9cab91959e | ||
|
|
bd9778a3d1 | ||
|
|
62a5fce66c | ||
|
|
2bd84636d6 | ||
|
|
ac561e7aca | ||
|
|
59daceef99 | ||
|
|
1dc41badd9 | ||
|
|
1a83315424 | ||
|
|
3c5776214f | ||
|
|
5fff0a2923 | ||
|
|
8df7f2992d | ||
|
|
7741394c9c | ||
|
|
25bd91debc | ||
|
|
7fe4724e10 | ||
|
|
123ae37524 | ||
|
|
172604fcdb | ||
|
|
7d887c73e8 | ||
|
|
6abaac25d9 | ||
|
|
cc897840e2 | ||
|
|
757840b76f | ||
|
|
50e8d1f8f4 | ||
|
|
65f8d38c59 | ||
|
|
e70c10adbd | ||
|
|
64ec73d821 | ||
|
|
32e8a37f33 | ||
|
|
5290713504 | ||
|
|
18cfb56b45 | ||
|
|
4b12ae1531 | ||
|
|
9df98edca5 | ||
|
|
c3af6acb2c | ||
|
|
7ba00cafd9 | ||
|
|
5aefb3bc59 | ||
|
|
212e55ffd8 | ||
|
|
bf54d38c91 | ||
|
|
cee2240cdc | ||
|
|
4c1ad868a9 | ||
|
|
f5b3b29d6d | ||
|
|
8f5449527d | ||
|
|
8f6d720454 | ||
|
|
56a4a7043d | ||
|
|
5762859906 | ||
|
|
608bac6854 | ||
|
|
723f9cd98c | ||
|
|
abf4d91703 | ||
|
|
d8392ad3eb | ||
|
|
39caad18a5 | ||
|
|
6437f7bb65 | ||
|
|
e232044157 | ||
|
|
f78b56ef0a | ||
|
|
ca3c9af3b8 | ||
|
|
b8b2e74151 | ||
|
|
1f8341ac42 | ||
|
|
0964f15475 | ||
|
|
63fd7957c6 | ||
|
|
65377ffd9e | ||
|
|
f79320c013 | ||
|
|
cf71ea26ec | ||
|
|
ee96c37c20 | ||
|
|
a86923aee1 | ||
|
|
e0f8d06152 | ||
|
|
4cb4ce298a | ||
|
|
36de61a57f | ||
|
|
6f2ca5bb89 | ||
|
|
940885768d | ||
|
|
fc577b4c3e | ||
|
|
bf10af2ae2 | ||
|
|
b4dfcf1bb4 | ||
|
|
0b0ba21852 | ||
|
|
42e0346e25 | ||
|
|
212db84d0b | ||
|
|
e6eb8accf2 | ||
|
|
8bd73c3afa | ||
|
|
5369a25fa2 | ||
|
|
eeae46a415 | ||
|
|
c0badbe96b | ||
|
|
2bb51c136a | ||
|
|
3cfa4ea6d6 | ||
|
|
f01adf5eb0 | ||
|
|
a0b92554e9 | ||
|
|
ac4c7e06e7 | ||
|
|
0f9a6f4340 | ||
|
|
9586a9c0dd | ||
|
|
f6563b265b | ||
|
|
7aea9473de | ||
|
|
3f059d7748 | ||
|
|
7e3c31c4b2 | ||
|
|
1707a9690a | ||
|
|
379ce917a9 | ||
|
|
299aaa2b68 | ||
|
|
5cf5e87fa8 | ||
|
|
55f22562eb | ||
|
|
272d911464 | ||
|
|
6beb34baa8 | ||
|
|
61de0b67fa | ||
|
|
aec8cec9b8 | ||
|
|
83b9573b52 | ||
|
|
21d99a1f24 | ||
|
|
1331479072 | ||
|
|
b472a36a9a | ||
|
|
3238fcdae7 | ||
|
|
cd2587b1fd | ||
|
|
879884a9fa | ||
|
|
5d3b963682 | ||
|
|
955e7a4f1c | ||
|
|
d2dcd4209d | ||
|
|
6299ff5b48 | ||
|
|
94a4dbaba1 | ||
|
|
c36deea045 | ||
|
|
7030d43aa5 | ||
|
|
aa02e9f8cf | ||
|
|
37e177b56e | ||
|
|
453f4da8ec | ||
|
|
400163b820 | ||
|
|
4ae9904c8a | ||
|
|
fe5e191cb5 | ||
|
|
d9d83df9de | ||
|
|
8dd8f88d2b | ||
|
|
01fd4c8ffa | ||
|
|
7ac3bb74e0 | ||
|
|
3b65cd0edc | ||
|
|
a9606728bf | ||
|
|
4d4f1a242c | ||
|
|
6b7143dd8f | ||
|
|
7e4ee00cb2 | ||
|
|
4868c45b43 | ||
|
|
81f485da6b | ||
|
|
18cbe51e6b | ||
|
|
149c8cc8b2 | ||
|
|
0dccb8c27b | ||
|
|
4302ea8832 | ||
|
|
1eac42dab8 | ||
|
|
9dd74f1f22 | ||
|
|
923ce74735 | ||
|
|
2d9f9adfee | ||
|
|
9a55e51a3a | ||
|
|
5681c917c5 | ||
|
|
6309e8bdf5 | ||
|
|
535efa3d73 | ||
|
|
b8a51d32f5 | ||
|
|
919b6b7014 | ||
|
|
971277ed39 | ||
|
|
7ce4de7a8b | ||
|
|
9591f4e14f | ||
|
|
27426b1390 | ||
|
|
fcb75dd780 | ||
|
|
1be9c9c1bd | ||
|
|
e088d053ab | ||
|
|
ffa8d9c063 | ||
|
|
7a5596a281 | ||
|
|
9f46f74357 | ||
|
|
36c4e2dfe0 | ||
|
|
5cb31dbe9d | ||
|
|
399fc98dec | ||
|
|
c22371e0c5 | ||
|
|
a4842c078b | ||
|
|
c332760786 | ||
|
|
ea4247c688 | ||
|
|
fec8c0cc14 | ||
|
|
9b585c73fb | ||
|
|
c695fa525f | ||
|
|
93f3e27d48 | ||
|
|
52ab7937bd | ||
|
|
762bfa8514 | ||
|
|
ca20996b62 | ||
|
|
ad14818de8 | ||
|
|
32839656f8 | ||
|
|
a48faad17a | ||
|
|
40487923f9 | ||
|
|
f1656c6d1e | ||
|
|
4c3dbbd8d5 | ||
|
|
4088ed747e | ||
|
|
bca8df8efd | ||
|
|
54f0a69596 | ||
|
|
9065c0d260 | ||
|
|
cb0150a0f9 | ||
|
|
ec0f7e3f7a | ||
|
|
e5d898f025 | ||
|
|
52bdb1cd6a | ||
|
|
49f9dfcf95 | ||
|
|
9536cdcae1 | ||
|
|
57e2632f38 | ||
|
|
b372f7ee84 | ||
|
|
70e8253b63 |
61
.github/ISSUE_TEMPLATE/bug-issue.yml
vendored
Normal file
61
.github/ISSUE_TEMPLATE/bug-issue.yml
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
name: 🐞 Bug report
|
||||
description: Create a new bug report.
|
||||
title: 'bug: <title>'
|
||||
labels: [bug]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
# ReVanced Manager bug report
|
||||
|
||||
Please check for existing issues [here](https://github.com/revanced/revanced-manager/labels/bug) before creating a new one.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Bug description
|
||||
description: |
|
||||
- Describe your bug in detail
|
||||
- Add steps to reproduce the bug if possible (Step 1. Download some files. Step 2. ...)
|
||||
- Add images and videos if possible
|
||||
- List selected patches if applicable
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Version of ReVanced Manager and version & name of application you tried to patch
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Installation type
|
||||
options:
|
||||
- Non-root
|
||||
- Root
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Device logs
|
||||
description: Export logs in ReVanced Manager settings.
|
||||
render: shell
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Patcher logs
|
||||
description: Export logs in "Patcher" screen.
|
||||
render: shell
|
||||
validations:
|
||||
required: false
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Acknowledgements
|
||||
description: Your issue will be closed if you don't follow the checklist below!
|
||||
options:
|
||||
- label: This request is not a duplicate of an existing issue.
|
||||
required: true
|
||||
- label: I have chosen an appropriate title.
|
||||
required: true
|
||||
- label: All requested information has been provided properly.
|
||||
required: true
|
||||
- label: The issue is solely related to the ReVanced Manager
|
||||
required: true
|
||||
109
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
109
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,109 +0,0 @@
|
||||
name: 🐞 Bug report
|
||||
description: Report a bug or an issue.
|
||||
title: 'bug: '
|
||||
labels: ['Bug report']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source
|
||||
width="256px"
|
||||
media="(prefers-color-scheme: dark)"
|
||||
srcset="https://raw.githubusercontent.com/revanced/revanced-manager/main/assets/revanced-headline/revanced-headline-vertical-dark.svg"
|
||||
>
|
||||
<img
|
||||
width="256px"
|
||||
src="https://raw.githubusercontent.com/revanced/revanced-manager/main/assets/revanced-headline/revanced-headline-vertical-light.svg"
|
||||
>
|
||||
</picture>
|
||||
<br>
|
||||
<a href="https://revanced.app/">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/revanced/revanced-manager/main/assets/revanced-logo/revanced-logo.svg" />
|
||||
<img height="24px" src="https://raw.githubusercontent.com/revanced/revanced-manager/main/assets/revanced-logo/revanced-logo.svg" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://github.com/ReVanced">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://i.ibb.co/dMMmCrW/Git-Hub-Mark.png" />
|
||||
<img height="24px" src="https://i.ibb.co/9wV3HGF/Git-Hub-Mark-Light.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="http://revanced.app/discord">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://reddit.com/r/revancedapp">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://t.me/app_revanced">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://x.com/revancedapp">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/93124920/270180600-7c1b38bf-889b-4d68-bd5e-b9d86f91421a.png">
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/93124920/270108715-d80743fa-b330-4809-b1e6-79fbdc60d09c.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.youtube.com/@ReVanced">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<br>
|
||||
<br>
|
||||
Continuing the legacy of Vanced
|
||||
</p>
|
||||
|
||||
# ReVanced Manager bug report
|
||||
|
||||
Before creating a new bug report, please keep the following in mind:
|
||||
|
||||
- **Do not submit a duplicate bug report**: Search for existing bug reports [here](https://github.com/ReVanced/revanced-manager/issues?q=label%3A%22Bug+report%22).
|
||||
- **Review the contribution guidelines**: Make sure your bug report adheres to it. You can find the guidelines [here](https://github.com/ReVanced/revanced-manager/blob/main/CONTRIBUTING.md).
|
||||
- **Do not use the issue page for support**: If you need help or have questions, check out other platforms on [revanced.app](https://revanced.app).
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Bug description
|
||||
description: |
|
||||
- Describe your bug in detail
|
||||
- Add steps to reproduce the bug if possible (Step 1. ... Step 2. ...)
|
||||
- Add images and videos if possible
|
||||
- List used patches, downloader and settings if applicable
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Patch logs
|
||||
description: Patch logs can be exported by clicking on the "Logs" button in the "Patcher" screen, when patching finishes.
|
||||
render: shell
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Debug logs
|
||||
description: Debug logs can be exported by clicking on "Export debug logs" in "Settings" > "Advanced".
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Acknowledgements
|
||||
description: Your bug report will be closed if you don't follow the checklist below.
|
||||
options:
|
||||
- label: I have checked all open and closed bug reports and this is not a duplicate.
|
||||
required: true
|
||||
- label: I have chosen an appropriate title.
|
||||
required: true
|
||||
- label: All requested information has been provided properly.
|
||||
required: true
|
||||
- label: The bug is only related to ReVanced Manager.
|
||||
required: true
|
||||
6
.github/ISSUE_TEMPLATE/config.yml
vendored
6
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,5 +1 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: 🗨 Discussions
|
||||
url: https://github.com/revanced/revanced-suggestions/discussions
|
||||
about: Have something unspecific to ReVanced Manager in mind? Search for or start a new discussion!
|
||||
blank_issues_enabled: false
|
||||
42
.github/ISSUE_TEMPLATE/feature-issue.yml
vendored
Normal file
42
.github/ISSUE_TEMPLATE/feature-issue.yml
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
name: ⭐ Feature request
|
||||
description: Create a new feature request.
|
||||
title: 'feat: <title>'
|
||||
labels: [feature request]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
# ReVanced Manager feature request
|
||||
|
||||
Please check for existing feature requests [here](https://github.com/revanced/revanced-manager/labels/bug) before creating a new one.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Feature description
|
||||
description: Describe your feature in detail.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Motivation
|
||||
description: Explain why the lack of it is a problem.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: In case there is something else you want to add.
|
||||
validations:
|
||||
required: false
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Acknowledgements
|
||||
description: Your issue will be closed if you don't follow the checklist below!
|
||||
options:
|
||||
- label: This request is not a duplicate of an existing issue.
|
||||
required: true
|
||||
- label: I have chosen an appropriate title.
|
||||
required: true
|
||||
- label: All requested information has been provided properly.
|
||||
required: true
|
||||
- label: The issue is solely related to the ReVanced Manager
|
||||
required: true
|
||||
107
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
107
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -1,107 +0,0 @@
|
||||
name: ⭐ Feature request
|
||||
description: Create a detailed request for a new feature.
|
||||
title: 'feat: '
|
||||
labels: ['Feature request']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source
|
||||
width="256px"
|
||||
media="(prefers-color-scheme: dark)"
|
||||
srcset="https://raw.githubusercontent.com/revanced/revanced-manager/main/assets/revanced-headline/revanced-headline-vertical-dark.svg"
|
||||
>
|
||||
<img
|
||||
width="256px"
|
||||
src="https://raw.githubusercontent.com/revanced/revanced-manager/main/assets/revanced-headline/revanced-headline-vertical-light.svg"
|
||||
>
|
||||
</picture>
|
||||
<br>
|
||||
<a href="https://revanced.app/">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/revanced/revanced-manager/main/assets/revanced-logo/revanced-logo.svg" />
|
||||
<img height="24px" src="https://raw.githubusercontent.com/revanced/revanced-manager/main/assets/revanced-logo/revanced-logo.svg" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://github.com/ReVanced">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://i.ibb.co/dMMmCrW/Git-Hub-Mark.png" />
|
||||
<img height="24px" src="https://i.ibb.co/9wV3HGF/Git-Hub-Mark-Light.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="http://revanced.app/discord">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://reddit.com/r/revancedapp">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://t.me/app_revanced">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://x.com/revancedapp">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/93124920/270180600-7c1b38bf-889b-4d68-bd5e-b9d86f91421a.png">
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/93124920/270108715-d80743fa-b330-4809-b1e6-79fbdc60d09c.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.youtube.com/@ReVanced">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<br>
|
||||
<br>
|
||||
Continuing the legacy of Vanced
|
||||
</p>
|
||||
|
||||
# ReVanced Manager feature request
|
||||
|
||||
Before creating a new feature request, please keep the following in mind:
|
||||
|
||||
- **Do not submit a duplicate feature request**: Search for existing feature requests [here](https://github.com/ReVanced/revanced-manager/issues?q=label%3A%22Feature+request%22).
|
||||
- **Review the contribution guidelines**: Make sure your feature request adheres to it. You can find the guidelines [here](https://github.com/ReVanced/revanced-manager/blob/main/CONTRIBUTING.md).
|
||||
- **Do not use the issue page for support**: If you need help or have questions, check out other platforms on [revanced.app](https://revanced.app).
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Feature description
|
||||
description: |
|
||||
- Describe your feature in detail
|
||||
- Add images, videos, links, examples, references, etc. if possible
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Motivation
|
||||
description: |
|
||||
A strong motivation is necessary for a feature request to be considered.
|
||||
|
||||
- Why should this feature be implemented?
|
||||
- What is the explicit use case?
|
||||
- What are the benefits?
|
||||
- What makes this feature important?
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
id: acknowledgements
|
||||
attributes:
|
||||
label: Acknowledgements
|
||||
description: Your feature request will be closed if you don't follow the checklist below.
|
||||
options:
|
||||
- label: I have checked all open and closed feature requests and this is not a duplicate
|
||||
required: true
|
||||
- label: I have chosen an appropriate title.
|
||||
required: true
|
||||
- label: All requested information has been provided properly.
|
||||
required: true
|
||||
- label: The feature request is only related to ReVanced Manager.
|
||||
required: true
|
||||
2
.github/config.yaml
vendored
2
.github/config.yaml
vendored
@@ -1,2 +1,2 @@
|
||||
firstPRMergeComment: >
|
||||
Thank you for contributing to ReVanced. Join us on [Discord](https://revanced.app/discord) to receive a role for your contribution.
|
||||
❤️ Thank you for contributing to ReVanced Manager. Join us on [Discord](https://revanced.app/discord) if you want to receive a contributor role.
|
||||
|
||||
31
.github/workflows/build_pull_request.yml
vendored
31
.github/workflows/build_pull_request.yml
vendored
@@ -1,31 +0,0 @@
|
||||
name: Build pull request
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
branches:
|
||||
- dev
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Cache Gradle
|
||||
uses: burrunan/gradle-cache-action@v1
|
||||
|
||||
- name: Build
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: ./gradlew assembleRelease --no-daemon
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: revanced-manager
|
||||
path: |
|
||||
app/build/outputs/apk/release/revanced-manager*.apk
|
||||
app/build/outputs/apk/release/revanced-manager*.apk.asc
|
||||
26
.github/workflows/open_pull_request.yml
vendored
26
.github/workflows/open_pull_request.yml
vendored
@@ -1,26 +0,0 @@
|
||||
name: Open a PR to main
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
MESSAGE: Merge branch `${{ github.head_ref || github.ref_name }}` to `main`
|
||||
|
||||
jobs:
|
||||
pull-request:
|
||||
name: Open pull request
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Open pull request
|
||||
uses: repo-sync/pull-request@v2
|
||||
with:
|
||||
destination_branch: 'main'
|
||||
pr_title: 'chore: ${{ env.MESSAGE }}'
|
||||
pr_body: 'This pull request will ${{ env.MESSAGE }}.'
|
||||
pr_draft: true
|
||||
44
.github/workflows/pr-build.yml
vendored
Normal file
44
.github/workflows/pr-build.yml
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
name: Build pull request
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- ".github/workflows/pr-build.yml"
|
||||
- "app/**"
|
||||
- "gradle/**"
|
||||
- "*.properties"
|
||||
- ".kts"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
|
||||
- name: Set up Gradle
|
||||
uses: gradle/actions/setup-gradle@v4
|
||||
|
||||
- name: Build with Gradle
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: ./gradlew assembleRelease --no-daemon -PnoProguard -PsignAsDebug
|
||||
|
||||
- name: Set env
|
||||
run: echo "COMMIT_HASH=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
|
||||
|
||||
- name: Add hash to APK
|
||||
run: mv app/build/outputs/apk/release/app-release.apk revanced-manager-${{ env.COMMIT_HASH }}.apk
|
||||
|
||||
- name: Upload build
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: revanced-manager
|
||||
path: revanced-manager-${{ env.COMMIT_HASH }}.apk
|
||||
57
.github/workflows/release-build.yml
vendored
Normal file
57
.github/workflows/release-build.yml
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
name: Release Build
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
attestations: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set env
|
||||
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
|
||||
- name: Set up Gradle
|
||||
uses: gradle/actions/setup-gradle@v4
|
||||
|
||||
- name: Build with Gradle
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: ./gradlew assembleRelease --no-daemon
|
||||
|
||||
- name: Sign APK
|
||||
id: sign_apk
|
||||
uses: ilharp/sign-android-release@v1
|
||||
with:
|
||||
releaseDir: ./app/build/outputs/apk/release/
|
||||
signingKey: ${{ secrets.SIGNING_KEYSTORE }}
|
||||
keyStorePassword: ${{ secrets.SIGNING_KEYSTORE_PASSWORD }}
|
||||
keyAlias: ${{ secrets.SIGNING_KEY_ALIAS }}
|
||||
keyPassword: ${{ secrets.SIGNING_KEY_PASSWORD }}
|
||||
|
||||
- name: Add version to APK
|
||||
run: mv ${{ steps.sign_apk.outputs.signedFile }} revanced-manager-${{ env.RELEASE_VERSION }}.apk
|
||||
|
||||
- name: Generate artifact attestation
|
||||
uses: actions/attest-build-provenance@v1
|
||||
with:
|
||||
subject-path: revanced-manager-${{ env.RELEASE_VERSION }}.apk
|
||||
|
||||
- name: Publish release APK
|
||||
uses: "marvinpinto/action-automatic-releases@latest"
|
||||
with:
|
||||
repo_token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
prerelease: false
|
||||
files: revanced-manager-${{ env.RELEASE_VERSION }}.apk
|
||||
80
.github/workflows/release.yml
vendored
80
.github/workflows/release.yml
vendored
@@ -1,80 +0,0 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Release
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
id-token: write
|
||||
attestations: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
|
||||
- name: Cache Gradle
|
||||
uses: burrunan/gradle-cache-action@v3
|
||||
|
||||
- name: Build
|
||||
env:
|
||||
GITHUB_ACTOR: ${{ github.actor }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: ./gradlew assembleRelease
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "lts/*"
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Import GPG key
|
||||
uses: crazy-max/ghaction-import-gpg@v6
|
||||
with:
|
||||
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
passphrase: ${{ secrets.GPG_PASSPHRASE }}
|
||||
fingerprint: ${{ vars.GPG_FINGERPRINT }}
|
||||
|
||||
- name: Setup keystore
|
||||
run: |
|
||||
echo "${{ secrets.KEYSTORE }}" | base64 --decode > "app/keystore.jks"
|
||||
|
||||
- name: Release API
|
||||
run: npx multi-semantic-release --tag-format 'api@${version}' --ignore-packages app
|
||||
env:
|
||||
GITHUB_ACTOR: ${{ github.actor }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Release
|
||||
id: release
|
||||
run: |
|
||||
echo "NEW_TAG=$(npx multi-semantic-release --tag-format 'v${version}' --ignore-packages api | tee | grep 'Created tag ' | sed -E 's/.*Created tag ([^ ]+).*/\1/')" >> $GITHUB_OUTPUT
|
||||
env:
|
||||
GITHUB_ACTOR: ${{ github.actor }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
|
||||
KEYSTORE_ENTRY_ALIAS: ${{ secrets.KEYSTORE_ENTRY_ALIAS }}
|
||||
KEYSTORE_ENTRY_PASSWORD: ${{ secrets.KEYSTORE_ENTRY_PASSWORD }}
|
||||
|
||||
- name: Attest
|
||||
if: steps.release.outputs.NEW_TAG != ''
|
||||
uses: actions/attest-build-provenance@v2
|
||||
with:
|
||||
subject-name: 'ReVanced Manager ${{ steps.release.outputs.NEW_TAG }}'
|
||||
subject-path: app/build/outputs/apk/release/revanced-manager*.apk
|
||||
@@ -16,4 +16,4 @@ jobs:
|
||||
token: ${{ secrets.DOCUMENTATION_REPO_ACCESS_TOKEN }}
|
||||
repository: revanced/revanced-documentation
|
||||
event-type: update-documentation
|
||||
client-payload: '{"repo": "${{ github.event.repository.name }}", "ref": "${{ github.ref }}"}'
|
||||
client-payload: '{"repo": "${{ github.event.repository.name }}", "ref": "${{ github.ref }}"}'
|
||||
136
.gitignore
vendored
136
.gitignore
vendored
@@ -1,132 +1,12 @@
|
||||
### Java template
|
||||
# Compiled class file
|
||||
*.class
|
||||
|
||||
# Log file
|
||||
*.log
|
||||
|
||||
# BlueJ files
|
||||
*.ctxt
|
||||
|
||||
# Mobile Tools for Java (J2ME)
|
||||
.mtj.tmp/
|
||||
|
||||
# Package Files #
|
||||
*.jar
|
||||
*.war
|
||||
*.nar
|
||||
*.ear
|
||||
*.zip
|
||||
*.tar.gz
|
||||
*.rar
|
||||
|
||||
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
|
||||
hs_err_pid*
|
||||
|
||||
### JetBrains template
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# User-specific stuff
|
||||
.idea/**/workspace.xml
|
||||
.idea/**/tasks.xml
|
||||
.idea/**/usage.statistics.xml
|
||||
.idea/**/dictionaries
|
||||
.idea/**/shelf
|
||||
|
||||
# Generated files
|
||||
.idea/**/contentModel.xml
|
||||
|
||||
# Sensitive or high-churn files
|
||||
.idea/**/dataSources/
|
||||
.idea/**/dataSources.ids
|
||||
.idea/**/dataSources.local.xml
|
||||
.idea/**/sqlDataSources.xml
|
||||
.idea/**/dynamic.xml
|
||||
.idea/**/uiDesigner.xml
|
||||
.idea/**/dbnavigator.xml
|
||||
|
||||
# Gradle
|
||||
.idea/**/gradle.xml
|
||||
.idea/**/libraries
|
||||
|
||||
# Gradle and Maven with auto-import
|
||||
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||
# since they will be recreated, and may cause churn. Uncomment if using
|
||||
# auto-import.
|
||||
.idea/artifacts
|
||||
.idea/compiler.xml
|
||||
.idea/jarRepositories.xml
|
||||
.idea/modules.xml
|
||||
.idea/*.iml
|
||||
.idea/modules
|
||||
*.iml
|
||||
*.ipr
|
||||
|
||||
# CMake
|
||||
cmake-build-*/
|
||||
|
||||
# Mongo Explorer plugin
|
||||
.idea/**/mongoSettings.xml
|
||||
|
||||
# File-based project format
|
||||
*.iws
|
||||
|
||||
# IntelliJ
|
||||
out/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Cursive Clojure plugin
|
||||
.idea/replstate.xml
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
|
||||
# Editor-based Rest Client
|
||||
.idea/httpRequests
|
||||
|
||||
# Android studio 3.1+ serialized cache file
|
||||
.idea/caches/build_file_checksums.ser
|
||||
|
||||
### Gradle template
|
||||
.gradle
|
||||
**/build/
|
||||
!src/**/build/
|
||||
|
||||
# Ignore Gradle GUI config
|
||||
gradle-app.setting
|
||||
|
||||
# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
|
||||
!gradle-wrapper.jar
|
||||
|
||||
# Cache of project
|
||||
.gradletasknamecache
|
||||
|
||||
# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898
|
||||
# gradle/wrapper/gradle-wrapper.properties
|
||||
|
||||
# Potentially copyrighted test APK
|
||||
*.apk
|
||||
|
||||
# Ignore vscode config
|
||||
.vscode/
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
|
||||
# Ignore IDEA files
|
||||
.idea/
|
||||
|
||||
.kotlin/
|
||||
|
||||
/local.properties
|
||||
/.idea
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
local.properties
|
||||
|
||||
.cxx
|
||||
.kotlin/
|
||||
|
||||
119
README.md
119
README.md
@@ -1,104 +1,55 @@
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source
|
||||
width="256px"
|
||||
media="(prefers-color-scheme: dark)"
|
||||
srcset="assets/revanced-headline/revanced-headline-vertical-dark.svg"
|
||||
>
|
||||
<img
|
||||
width="256px"
|
||||
src="assets/revanced-headline/revanced-headline-vertical-light.svg"
|
||||
>
|
||||
</picture>
|
||||
<br>
|
||||
<a href="https://revanced.app/">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="assets/revanced-logo/revanced-logo.svg" />
|
||||
<img height="24px" src="assets/revanced-logo/revanced-logo.svg" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://github.com/ReVanced">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://i.ibb.co/dMMmCrW/Git-Hub-Mark.png" />
|
||||
<img height="24px" src="https://i.ibb.co/9wV3HGF/Git-Hub-Mark-Light.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="http://revanced.app/discord">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://reddit.com/r/revancedapp">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://t.me/app_revanced">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://x.com/revancedapp">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/93124920/270180600-7c1b38bf-889b-4d68-bd5e-b9d86f91421a.png">
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/93124920/270108715-d80743fa-b330-4809-b1e6-79fbdc60d09c.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.youtube.com/@ReVanced">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<br>
|
||||
<br>
|
||||
Continuing the legacy of Vanced
|
||||
</p>
|
||||
# ReVanced Manager (Compose Rewrite)
|
||||
|
||||
# 💊 ReVanced Manager
|
||||
[](../../blob/main/LICENSE)
|
||||
[](https://github.com/ReVanced/revanced-manager/commits/compose-dev)
|
||||
[](https://github.com/ReVanced/revanced-manager/commits/compose-dev)
|
||||
|
||||

|
||||

|
||||
_(Yet another)_ rewrite of the ReVanced Manager using Kotlin and Jetpack Compose.
|
||||
|
||||
Application to use ReVanced on Android
|
||||
## Design system
|
||||
|
||||
## ❓ About
|
||||
In this rewrite, we are adopting the latest Material Design principles and guidelines by using Material 3 and Material You.
|
||||
|
||||
ReVanced Manager is an application that uses [ReVanced Patcher](https://github.com/revanced/revanced-patcher) to patch Android apps.
|
||||
Material Design is a design system developed by Google that provides a unified visual language for building beautiful and consistent user interfaces across all platforms and devices. Material You is an extension of Material Design that provides even more customization options for users, making it possible for them to personalize their device and create a unique look and feel.
|
||||
|
||||
## 💪 Features
|
||||
### Why Material 3?
|
||||
|
||||
Some of the features ReVanced Manager provides are:
|
||||
* **Consistent design language**
|
||||
* **Improved accessibility**
|
||||
* **Better user experience**
|
||||
|
||||
- ⬇️ **Download**: Automatically download apps using the ReVanced Manager downloader plugin system
|
||||
- 💉 **Patch**: Select and apply patches to any Android app
|
||||
- 🛠️ **Customize**: Manage patches, apps, signing, themes, updates, and many more settings
|
||||
By using Material 3 and Material You, we are ensuring that the app's user interface is consistent, customizable, accessible, and engaging for our users. This will help to improve the overall user experience and increase user satisfaction with the the manager.
|
||||
|
||||
## Technology stack
|
||||
|
||||
* Kotlin: Kotlin is a modern and concise programming language that is fully interoperable with Java and provides improved safety, readability, and maintainability compared to Java.
|
||||
* Jetpack Compose: Jetpack Compose is a modern UI toolkit for Android development that allows developers to build beautiful and performant user interfaces using declarative programming. It provides a unified and efficient way of building UI that is well-integrated with the Android framework.
|
||||
|
||||
## Why Kotlin and Compose?
|
||||
|
||||
* **Improved safety:** Kotlin provides improved safety compared to Java, which reduces the likelihood of common programming mistakes that can cause security vulnerabilities or crashes.
|
||||
* **Concise and readable code:** Kotlin's concise syntax and expressive type system make the code more readable, which makes it easier for developers to understand and maintain the codebase.
|
||||
* **Better performance:** Jetpack Compose uses the power of the Android framework to provide smooth and fast performance, which enhances the user experience.
|
||||
* **Modern and efficient UI development:** Jetpack Compose provides a modern and efficient way of building UI, which makes it easier for developers to create beautiful and performant user interfaces.
|
||||
|
||||
## 🔽 Download
|
||||
|
||||
You can download the most recent version of ReVanced Manager at [revanced.app/download](https://revanced.app/download) or from [GitHub releases](https://github.com/ReVanced/revanced-manager/releases/latest).
|
||||
Learn how to use ReVanced Manager by following the [documentation](/docs).
|
||||
You can obtain ReVanced Manager by downloading it from either [revanced.app/download](https://revanced.app/download) or [GitHub Releases](https://github.com/ReVanced/revanced-manager/releases)
|
||||
|
||||
## 📚 Everything else
|
||||
## 📝 Prerequisites
|
||||
|
||||
### 📙 Contributing
|
||||
For a list of prerequisites, refer to [docs/0_prerequisites.md](docs/0_prerequisites.md)
|
||||
|
||||
Thank you for considering contributing to ReVanced Manager.
|
||||
You can find the contribution guidelines [here](CONTRIBUTING.md).
|
||||
## 🔴 Issues
|
||||
|
||||
### 🛠️ Building
|
||||
For suggestions and bug reports, open an issue [here](https://github.com/revanced/revanced-manager/issues/new/choose).
|
||||
|
||||
To build a ReVanced Manager, you can follow the [documentation](/docs).
|
||||
## 🌐 Translation
|
||||
|
||||
### 📄 Documentation
|
||||
[](https://crowdin.com/project/revanced)
|
||||
|
||||
You can find the documentation for ReVanced Manager [here](/docs).
|
||||
We're accepting translations on [Crowdin](https://translate.revanced.app)
|
||||
|
||||
## ⚖️ License
|
||||
## 🛠️ Building Manager from source
|
||||
|
||||
ReVanced Manager is licensed under the GPLv3 license. Please see the [license file](LICENSE) for more information.
|
||||
[tl;dr](https://www.tldrlegal.com/license/gnu-general-public-license-v3-gpl-3) you may copy, distribute and modify ReVanced Manager as long as you track changes/dates in source files.
|
||||
Any modifications to ReVanced Manager must also be made available under the GPL, along with build & install instructions.
|
||||
For instructions on how to build ReVanced Manager from source, refer to [docs/4_building.md](docs/4_building.md)
|
||||
@@ -58,46 +58,20 @@
|
||||
Continuing the legacy of Vanced
|
||||
</p>
|
||||
|
||||
# 👋 Contribution guidelines
|
||||
# 🔒 Security Policy
|
||||
|
||||
This document describes how to contribute to ReVanced Manager.
|
||||
This document describes how to report security vulnerabilities for ReVanced Manager.
|
||||
|
||||
## 📖 Resources to help you get started
|
||||
## 🚨 Reporting a Vulnerability
|
||||
|
||||
* The [documentation](/docs/README.md) provides steps to build ReVanced Manager from source
|
||||
* Our [backlog](https://github.com/orgs/ReVanced/projects/12) is where we keep track of what we're working on
|
||||
* [Issues](https://github.com/ReVanced/revanced-manager/issues) are where we keep track of bugs and feature requests
|
||||
Please open an issue in our [advisory tracker](https://github.com/ReVanced/revanced-manager/security/advisories/new) or reach out privately to us on [Discord](https://discord.gg/revanced).
|
||||
|
||||
## 🙏 Submitting a feature request
|
||||
If a vulnerability is confirmed and accepted, you can join our [Discord](https://discord.gg/revanced) server to receive a special contributor role.
|
||||
|
||||
Features can be requested by opening an issue using the
|
||||
[Feature request issue template](https://github.com/ReVanced/revanced-manager/issues/new?assignees=&labels=Feature+request&projects=&template=feature_request.yml&title=feat%3A+).
|
||||
### ⏳ Supported Versions
|
||||
|
||||
> **Note**
|
||||
> Requests can be accepted or rejected at the discretion of maintainers of ReVanced Manager.
|
||||
> Good motivation has to be provided for a request to be accepted.
|
||||
|
||||
## 🐞 Submitting a bug report
|
||||
|
||||
If you encounter a bug while using ReVanced Manager, open an issue using the
|
||||
[Bug report issue template](https://github.com/ReVanced/revanced-manager/issues/new?assignees=&labels=Bug+report&projects=&template=bug_report.yml&title=bug%3A+).
|
||||
|
||||
## 📝 How to contribute
|
||||
|
||||
1. Before contributing, it is recommended to open an issue to discuss your change
|
||||
with the maintainers of ReVanced Manager. This will help you determine whether your change is acceptable
|
||||
and whether it is worth your time to implement it
|
||||
2. Development happens on the `dev` branch. Fork the repository and create your branch from `dev`
|
||||
3. Commit your changes
|
||||
4. Submit a pull request to the `dev` branch of the repository and reference issues
|
||||
that your pull request closes in the description of your pull request
|
||||
5. Our team will review your pull request and provide feedback. Once your pull request is approved,
|
||||
it will be merged into the `dev` branch and will be included in the next release of ReVanced Manager
|
||||
|
||||
## 🤚 I want to contribute but don't know how to code
|
||||
|
||||
Even if you don't know how to code, you can still contribute by
|
||||
translating ReVanced Manager on [Crowdin](https://translate.revanced.app/).
|
||||
|
||||
❤️ Thank you for considering contributing to ReVanced Manager,
|
||||
ReVanced
|
||||
| Version | Branch | Supported |
|
||||
| --------------------------------------------------------------------------------------------------------------------------------------- | ----------- | ------------------ |
|
||||
|  | main | :white_check_mark: |
|
||||
|  | dev | :white_check_mark: |
|
||||
|  | compose-dev | :white_check_mark: |
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"info": "This is verification file for ads.fund project",
|
||||
"project": {
|
||||
"name": "ReVanced Manager",
|
||||
"walletAddress": "0x7ab4091e00363654bf84B34151225742cd92FCE5",
|
||||
"tokenAddress": "0xadf954bc6f509b3a32fb5e97ed4ba6c000e37155"
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
{
|
||||
"branches": [
|
||||
"main",
|
||||
{
|
||||
"name": "dev",
|
||||
"prerelease": true
|
||||
}
|
||||
],
|
||||
"plugins": [
|
||||
[
|
||||
"@semantic-release/commit-analyzer",
|
||||
{
|
||||
"releaseRules": [
|
||||
{ "type": "build", "scope": "Needs bump", "release": "patch" }
|
||||
]
|
||||
}
|
||||
],
|
||||
"@semantic-release/release-notes-generator",
|
||||
"@semantic-release/changelog",
|
||||
"gradle-semantic-release-plugin",
|
||||
[
|
||||
"@semantic-release/git",
|
||||
{
|
||||
"assets": [
|
||||
"CHANGELOG.md",
|
||||
"gradle.properties"
|
||||
],
|
||||
"message": "chore: Release API v${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
|
||||
}
|
||||
],
|
||||
[
|
||||
"@saithodev/semantic-release-backmerge",
|
||||
{
|
||||
"backmergeBranches": [{"from": "main", "to": "dev"}],
|
||||
"clearWorkspace": true
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
plugins {
|
||||
alias(libs.plugins.android.library)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.kotlin.parcelize)
|
||||
alias(libs.plugins.compose.compiler)
|
||||
alias(libs.plugins.binary.compatibility.validator)
|
||||
`maven-publish`
|
||||
signing
|
||||
}
|
||||
|
||||
group = "app.revanced"
|
||||
|
||||
dependencies {
|
||||
implementation(libs.androidx.ktx)
|
||||
implementation(libs.runtime.ktx)
|
||||
implementation(libs.activity.compose)
|
||||
implementation(libs.appcompat)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "app.revanced.manager.plugin.downloader"
|
||||
compileSdk = 35
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 26
|
||||
|
||||
consumerProguardFiles("consumer-rules.pro")
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "17"
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
aidl = true
|
||||
}
|
||||
}
|
||||
|
||||
apiValidation {
|
||||
nonPublicMarkers += "app.revanced.manager.plugin.downloader.PluginHostApi"
|
||||
}
|
||||
|
||||
publishing {
|
||||
repositories {
|
||||
maven {
|
||||
name = "GitHubPackages"
|
||||
url = uri("https://maven.pkg.github.com/revanced/revanced-manager")
|
||||
credentials {
|
||||
username = System.getenv("GITHUB_ACTOR") ?: extra["gpr.user"] as String?
|
||||
password = System.getenv("GITHUB_TOKEN") ?: extra["gpr.key"] as String?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
publications {
|
||||
create<MavenPublication>("Api") {
|
||||
afterEvaluate {
|
||||
from(components["release"])
|
||||
}
|
||||
|
||||
groupId = "app.revanced"
|
||||
artifactId = "revanced-manager-api"
|
||||
version = project.version.toString()
|
||||
|
||||
pom {
|
||||
name = "ReVanced Manager API"
|
||||
description = "API for ReVanced Manager."
|
||||
url = "https://revanced.app"
|
||||
|
||||
licenses {
|
||||
license {
|
||||
name = "GNU General Public License v3.0"
|
||||
url = "https://www.gnu.org/licenses/gpl-3.0.en.html"
|
||||
}
|
||||
}
|
||||
developers {
|
||||
developer {
|
||||
id = "ReVanced"
|
||||
name = "ReVanced"
|
||||
email = "contact@revanced.app"
|
||||
}
|
||||
}
|
||||
scm {
|
||||
connection = "scm:git:git://github.com/revanced/revanced-manager.git"
|
||||
developerConnection = "scm:git:git@github.com:revanced/revanced-manager.git"
|
||||
url = "https://github.com/revanced/revanced-manager"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
signing {
|
||||
useGpgCmd()
|
||||
sign(publishing.publications["Api"])
|
||||
}
|
||||
1
api/gradlew
vendored
1
api/gradlew
vendored
@@ -1 +0,0 @@
|
||||
../gradlew
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"name": "api",
|
||||
"private": false,
|
||||
"devDependencies": {
|
||||
"@anolilab/multi-semantic-release": "^1.1.10",
|
||||
"@saithodev/semantic-release-backmerge": "^4.0.1",
|
||||
"@semantic-release/changelog": "^6.0.3",
|
||||
"@semantic-release/git": "^10.0.1",
|
||||
"gradle-semantic-release-plugin": "^1.10.1"
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
{
|
||||
"branches": [
|
||||
"main",
|
||||
{
|
||||
"name": "dev",
|
||||
"prerelease": true
|
||||
}
|
||||
],
|
||||
"plugins": [
|
||||
[
|
||||
"@semantic-release/commit-analyzer",
|
||||
{
|
||||
"releaseRules": [
|
||||
{ "type": "build", "scope": "Needs bump", "release": "patch" }
|
||||
]
|
||||
}
|
||||
],
|
||||
"@semantic-release/release-notes-generator",
|
||||
"@semantic-release/changelog",
|
||||
"gradle-semantic-release-plugin",
|
||||
[
|
||||
"@semantic-release/git",
|
||||
{
|
||||
"assets": [
|
||||
"CHANGELOG.md",
|
||||
"gradle.properties"
|
||||
],
|
||||
"message": "chore: Release v${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
|
||||
}
|
||||
],
|
||||
[
|
||||
"@semantic-release/github",
|
||||
{
|
||||
"assets": [
|
||||
{
|
||||
"path": "build/outputs/apk/release/revanced-manager*.apk?(.asc)"
|
||||
}
|
||||
],
|
||||
"successComment": false
|
||||
}
|
||||
],
|
||||
[
|
||||
"@saithodev/semantic-release-backmerge",
|
||||
{
|
||||
"backmergeBranches": [{"from": "main", "to": "dev"}],
|
||||
"clearWorkspace": true
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
380
app/CHANGELOG.md
380
app/CHANGELOG.md
@@ -1,380 +0,0 @@
|
||||
# app [1.26.0-dev.16](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.15...v1.26.0-dev.16) (2025-12-30)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Show patches as individual steps in patcher screen ([#2889](https://github.com/ReVanced/revanced-manager/issues/2889)) ([11dd6e4](https://github.com/ReVanced/revanced-manager/commit/11dd6e4064099427a8c9bc6f225a19412e5c70e2))
|
||||
|
||||
# app [1.26.0-dev.15](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.14...v1.26.0-dev.15) (2025-12-29)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* install dialog getting stuck ([#2900](https://github.com/ReVanced/revanced-manager/issues/2900)) ([18a4df9](https://github.com/ReVanced/revanced-manager/commit/18a4df9af9cac120fdb8e4ff7aadd2e2a8d5c1a6))
|
||||
|
||||
# app [1.26.0-dev.14](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.13...v1.26.0-dev.14) (2025-12-28)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Update selected patch count when SelectionState changes ([#2896](https://github.com/ReVanced/revanced-manager/issues/2896)) ([0d26df0](https://github.com/ReVanced/revanced-manager/commit/0d26df03f463195dae550240c7f652680763079c))
|
||||
|
||||
# app [1.26.0-dev.13](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.12...v1.26.0-dev.13) (2025-12-17)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Make patcher screen design more consistent with inspiration ([#2805](https://github.com/ReVanced/revanced-manager/issues/2805)) ([dbb6c01](https://github.com/ReVanced/revanced-manager/commit/dbb6c01e89a5e710185ff4304de0ac9e19bed053))
|
||||
|
||||
# app [1.26.0-dev.12](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.11...v1.26.0-dev.12) (2025-12-17)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Improve trust plugin dialog design ([#2420](https://github.com/ReVanced/revanced-manager/issues/2420)) ([0300da9](https://github.com/ReVanced/revanced-manager/commit/0300da9eac6c0fc29dbbb66622c0d52f4cf68934))
|
||||
|
||||
# app [1.26.0-dev.11](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.10...v1.26.0-dev.11) (2025-10-25)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add pure black theme ([#2824](https://github.com/ReVanced/revanced-manager/issues/2824)) ([3d75ffe](https://github.com/ReVanced/revanced-manager/commit/3d75ffe6a7a39efdebe13dbd07c937c1de409ead))
|
||||
|
||||
# app [1.26.0-dev.10](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.9...v1.26.0-dev.10) (2025-10-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* prevent back presses during installation ([2ff7072](https://github.com/ReVanced/revanced-manager/commit/2ff70728b490b92f212a82dcf599bc0c23f589e7))
|
||||
|
||||
# app [1.26.0-dev.9](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.8...v1.26.0-dev.9) (2025-10-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Instantly re-fetch patch bundle on pre-release preference update ([d5671db](https://github.com/ReVanced/revanced-manager/commit/d5671db3a77541c07bbbb4c3baca02f3ba0703f2)), closes [#2784](https://github.com/ReVanced/revanced-manager/issues/2784)
|
||||
|
||||
# app [1.26.0-dev.8](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.7...v1.26.0-dev.8) (2025-10-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Offcenter loading indicator in AppSelector ([12d92ba](https://github.com/ReVanced/revanced-manager/commit/12d92ba8110f5d1ac78aeecfa575444b5c53f561))
|
||||
|
||||
# app [1.26.0-dev.7](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.6...v1.26.0-dev.7) (2025-10-03)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Improve consistency between pre-release toggles ([e1b768c](https://github.com/ReVanced/revanced-manager/commit/e1b768c4679ecae8bff8007bdab56ff6544b12b6))
|
||||
|
||||
# app [1.26.0-dev.6](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.5...v1.26.0-dev.6) (2025-10-03)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Broken version comparison ([c327857](https://github.com/ReVanced/revanced-manager/commit/c3278578237dcddd9e7ab79ee80a02fdeef9604d))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Open contributor's GitHub profile when clicked ([#2775](https://github.com/ReVanced/revanced-manager/issues/2775)) ([2571cb8](https://github.com/ReVanced/revanced-manager/commit/2571cb8c1108e9c1ed84950f17692c09d66e0556))
|
||||
|
||||
# app [1.26.0-dev.5](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.4...v1.26.0-dev.5) (2025-10-03)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Toggle to use pre-release versions of ReVanced Patches ([08cec67](https://github.com/ReVanced/revanced-manager/commit/08cec674bbbe5297090ac5ee6039569975fbe9e7))
|
||||
|
||||
# app [1.26.0-dev.4](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.3...v1.26.0-dev.4) (2025-10-03)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add newlines to debug logs ([4753873](https://github.com/ReVanced/revanced-manager/commit/4753873866b575e2dcb160020df63f63862c8f33))
|
||||
|
||||
# app [1.26.0-dev.3](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.2...v1.26.0-dev.3) (2025-10-03)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Toggle to use pre-release versions of ReVanced Manager ([#2773](https://github.com/ReVanced/revanced-manager/issues/2773)) ([d758964](https://github.com/ReVanced/revanced-manager/commit/d7589647426b3d3438161a2f0b59bf4f154ac34b))
|
||||
|
||||
# app [1.26.0-dev.2](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.1...v1.26.0-dev.2) (2025-10-03)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Migration of keystore, by fixing mislabeling of alias as cn ([#2769](https://github.com/ReVanced/revanced-manager/issues/2769)) ([aeab639](https://github.com/ReVanced/revanced-manager/commit/aeab639b2b09e8bbd2478cfbf5a518586405c0f7))
|
||||
|
||||
# app [1.26.0-dev.1](https://github.com/ReVanced/revanced-manager/compare/v1.25.1...v1.26.0-dev.1) (2025-10-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* `ExtendedFloatingActionButton` not accessible by screen readers ([#2080](https://github.com/ReVanced/revanced-manager/issues/2080)) ([e4f19b0](https://github.com/ReVanced/revanced-manager/commit/e4f19b0c251e818cce59e11362a29dc8f657e065))
|
||||
* add bounds checks in patch selector ([483be5d](https://github.com/ReVanced/revanced-manager/commit/483be5d722db2be2595f6f6dd0c537a6c8487daf))
|
||||
* Add missing header for "Updates" settings ([#2642](https://github.com/ReVanced/revanced-manager/issues/2642)) ([d4d2056](https://github.com/ReVanced/revanced-manager/commit/d4d2056585ccd4a0456318448dc822c0f40c9c50))
|
||||
* Allow different app version when downloading via plugin if setting is off ([#2579](https://github.com/ReVanced/revanced-manager/issues/2579)) ([59d233e](https://github.com/ReVanced/revanced-manager/commit/59d233e15c885104900c7d4129fb4839c4da81e0))
|
||||
* always use default patch selection if customization is disabled ([cc77181](https://github.com/ReVanced/revanced-manager/commit/cc771817cba3dfd8f704cb7ecc9089ad7911c6ce))
|
||||
* android icon not loading in app selector ([deea682](https://github.com/ReVanced/revanced-manager/commit/deea68265157da65ef98986d751e2551797522e0))
|
||||
* automatically focus search views ([d23d673](https://github.com/ReVanced/revanced-manager/commit/d23d673c4703cdfa3be3a292873bbb37bea30ac7))
|
||||
* available updates dialog list item color ([1a54313](https://github.com/ReVanced/revanced-manager/commit/1a54313c1dc4efbb8b274201a79e28661a7ecf64))
|
||||
* Broken header padding in `AlertDialogExtended` when using an Icon ([8d939a6](https://github.com/ReVanced/revanced-manager/commit/8d939a6669909a44382fc7404276f2eeefcf728d))
|
||||
* broken logo in about page on release builds ([ad775f3](https://github.com/ReVanced/revanced-manager/commit/ad775f3059345dd93ff2baf6d018c2beecc413df))
|
||||
* buildfile syntax ([#66](https://github.com/ReVanced/revanced-manager/issues/66)) ([5c17a78](https://github.com/ReVanced/revanced-manager/commit/5c17a78e46db586642d53362267472fbbd47ae8c))
|
||||
* bundles not loading on Android 14 ([56896d6](https://github.com/ReVanced/revanced-manager/commit/56896d6197baa836bcd4a499ea2cee487e3d07c8))
|
||||
* Change the title in the Update screen from "Updates" to "Update" ([5f23769](https://github.com/ReVanced/revanced-manager/commit/5f2376919bd036987eba8188e3a1a2ff53ef6793)), closes [#1960](https://github.com/ReVanced/revanced-manager/issues/1960)
|
||||
* cleanup advanced settings screen ([02ea5c6](https://github.com/ReVanced/revanced-manager/commit/02ea5c6d4a2e6baa7c034b614deb6e4232cf6d0b))
|
||||
* **Compose:** Adjusted universal patches safeguard and warnings ([#2550](https://github.com/ReVanced/revanced-manager/issues/2550)) ([663cf2d](https://github.com/ReVanced/revanced-manager/commit/663cf2d6b86c276c6bb236af8e05a4f69df9eba0))
|
||||
* contributors screen fix ([#1256](https://github.com/ReVanced/revanced-manager/issues/1256)) ([dc73462](https://github.com/ReVanced/revanced-manager/commit/dc73462ac41bd5f1813358eb5e2265a3e2e7c0f9))
|
||||
* contributors screen repository name ([426b289](https://github.com/ReVanced/revanced-manager/commit/426b28932fe37a6d7412685819ffc8e26b69d31c))
|
||||
* Correct preference description ([#2619](https://github.com/ReVanced/revanced-manager/issues/2619)) ([0096169](https://github.com/ReVanced/revanced-manager/commit/0096169af8f9e2db6c22b8e88f0dfe1cab1260be))
|
||||
* Correctly display universal patches warning ([#2570](https://github.com/ReVanced/revanced-manager/issues/2570)) ([24c4cd3](https://github.com/ReVanced/revanced-manager/commit/24c4cd3f991953dd00b5bf5e7c3ec965315a9528))
|
||||
* correctly patch apk files ([c5cb18a](https://github.com/ReVanced/revanced-manager/commit/c5cb18a7eab838ea096577780335a29b9771b43d))
|
||||
* crash caused by compose inlining bug ([05fe058](https://github.com/ReVanced/revanced-manager/commit/05fe0581516a373cc26dd559d3fc7f21fcf16f3f))
|
||||
* crash when removing used bundles ([189c993](https://github.com/ReVanced/revanced-manager/commit/189c993ada6406db6f8c48c4051c5bd9fac98e2b))
|
||||
* delete temporary files ([#1341](https://github.com/ReVanced/revanced-manager/issues/1341)) ([b03f7b1](https://github.com/ReVanced/revanced-manager/commit/b03f7b18a029465142d08fe1ed68e92c81586a5f))
|
||||
* disable `WebView` history ([#1278](https://github.com/ReVanced/revanced-manager/issues/1278)) ([a811df9](https://github.com/ReVanced/revanced-manager/commit/a811df9547da33fc61397cb33ba5fd35ee470ff9))
|
||||
* display version from manifest ([#2634](https://github.com/ReVanced/revanced-manager/issues/2634)) ([1fb94b7](https://github.com/ReVanced/revanced-manager/commit/1fb94b711fdbbbca7d9baaa90c53faf208fc4d0d))
|
||||
* Do not poll battery optimization status ([#2491](https://github.com/ReVanced/revanced-manager/issues/2491)) ([26778f5](https://github.com/ReVanced/revanced-manager/commit/26778f57e6dd185d9aed1086aa03659a2e91d1a9))
|
||||
* don't store app list in parcel ([e7802ed](https://github.com/ReVanced/revanced-manager/commit/e7802ed3d714cbe6e29409d27989c65d4d7ce6a5))
|
||||
* dont crash when the bundle cannot be downloaded ([4d201f1](https://github.com/ReVanced/revanced-manager/commit/4d201f17f2ce01aad6adb456a49c3f03526c5ad3))
|
||||
* **downloader:** versions not loading correctly ([16c4290](https://github.com/ReVanced/revanced-manager/commit/16c4290f05d94cbe53e68cb98307d7be1bfce7af))
|
||||
* handle edge-to-edge properly in fullscreen dialogs ([eba92e2](https://github.com/ReVanced/revanced-manager/commit/eba92e2644663b10e7e17f2cf955afefe260d769))
|
||||
* handle exceptions when checking for bundle updates ([1dd6738](https://github.com/ReVanced/revanced-manager/commit/1dd673896454710094e83789abb585c106ee6bcb))
|
||||
* Handle open source licenses page crash ([#2569](https://github.com/ReVanced/revanced-manager/issues/2569)) ([f2ea007](https://github.com/ReVanced/revanced-manager/commit/f2ea00757a76ed8758bc0d4df54843c89483c986))
|
||||
* hide patch button ([#1284](https://github.com/ReVanced/revanced-manager/issues/1284)) ([dadc546](https://github.com/ReVanced/revanced-manager/commit/dadc5462e352e91cf971395def91d693677701bc))
|
||||
* Ignore long click when already in delete mode ([6f6296b](https://github.com/ReVanced/revanced-manager/commit/6f6296b8cde56d5fc73e00ef671ca7ab431455f4)), closes [#2503](https://github.com/ReVanced/revanced-manager/issues/2503)
|
||||
* import bundles on another thread ([0383bd7](https://github.com/ReVanced/revanced-manager/commit/0383bd74f73a3523d539c44cdf38b0e857c16bdc))
|
||||
* import export screen UX ([69c119d](https://github.com/ReVanced/revanced-manager/commit/69c119d545ac811c605124173e5cbc97a9064c79))
|
||||
* Improve background running notification ([#2614](https://github.com/ReVanced/revanced-manager/issues/2614)) ([05444d8](https://github.com/ReVanced/revanced-manager/commit/05444d8824a429c7e554d0597f8997e670936a63))
|
||||
* improve bundle page strings ([2a63a61](https://github.com/ReVanced/revanced-manager/commit/2a63a6163a8d2e6ee649cb22099b426ed605de8f))
|
||||
* improve keystore import error handling and show toast ([cd142a7](https://github.com/ReVanced/revanced-manager/commit/cd142a70d3f210161d3c1f20d2cb82a70432469f))
|
||||
* Inconsistent padding for battery optimisation warning ([6c3a99a](https://github.com/ReVanced/revanced-manager/commit/6c3a99a4921ab4438a038ad4c4bccd0326fdd565))
|
||||
* **installer:** make the correct column scrollable ([64496bf](https://github.com/ReVanced/revanced-manager/commit/64496bfbe77a9a44f5535fd5f12eee803ac7c26a))
|
||||
* **installer:** progress tracking ([f547bb7](https://github.com/ReVanced/revanced-manager/commit/f547bb7ab1b7149d7290729527714168a2561b23))
|
||||
* **installer:** properly track worker state ([#32](https://github.com/ReVanced/revanced-manager/issues/32)) ([de1ef23](https://github.com/ReVanced/revanced-manager/commit/de1ef23824227796c8583242e624f83d9dae5af3))
|
||||
* **installer:** save step incorrectly being marked as completed ([0264308](https://github.com/ReVanced/revanced-manager/commit/0264308b6dad051db80da6f130e8d28d86b38f04))
|
||||
* **installer:** sign and install on threads ([3d59ee5](https://github.com/ReVanced/revanced-manager/commit/3d59ee51acc5a6ebb17f68c0462d17d7ecb0f07c))
|
||||
* jvm signature clash error ([ee0f342](https://github.com/ReVanced/revanced-manager/commit/ee0f34245636027d55bd5bdfce4d6a5e6c3b3dcd))
|
||||
* library info not being embedded ([8c9fe69](https://github.com/ReVanced/revanced-manager/commit/8c9fe6989fc6d05afd53baa877f1e6dffc067b50))
|
||||
* load patch bundles earlier ([a2f9e2f](https://github.com/ReVanced/revanced-manager/commit/a2f9e2f1da961a13b2b20e2812593031c9339b88))
|
||||
* Match "Installation incompatible" dialog message with Flutter Manager ([#2231](https://github.com/ReVanced/revanced-manager/issues/2231)) ([fedaedf](https://github.com/ReVanced/revanced-manager/commit/fedaedfda112260144b0b9b0776509ddb3438046))
|
||||
* minify crash on building release ([#1245](https://github.com/ReVanced/revanced-manager/issues/1245)) ([6561e4c](https://github.com/ReVanced/revanced-manager/commit/6561e4c97c19134b22b72e19fad3884f99327b9a))
|
||||
* more android 34 fixes ([7fb1e27](https://github.com/ReVanced/revanced-manager/commit/7fb1e27617b69803b3d4463993b2290877502545))
|
||||
* move battery warning to dashboard ([3a05150](https://github.com/ReVanced/revanced-manager/commit/3a05150fa33f119ecdf436f8508862ef81c327a0))
|
||||
* Move temporary files outside of the cache directory ([#2122](https://github.com/ReVanced/revanced-manager/issues/2122)) ([b93ecc0](https://github.com/ReVanced/revanced-manager/commit/b93ecc0db20339393e1296c44ce4b1dbd837b577))
|
||||
* Offset badge ([c73fdfd](https://github.com/ReVanced/revanced-manager/commit/c73fdfdd2d3a1b8552d9c26df575b3019346596d))
|
||||
* only perform haptics on events ([e55566d](https://github.com/ReVanced/revanced-manager/commit/e55566d3df25480260922f0418b4bbee5d7b7a07))
|
||||
* option state crash ([#1456](https://github.com/ReVanced/revanced-manager/issues/1456)) ([f183b6d](https://github.com/ReVanced/revanced-manager/commit/f183b6d8a6b139fe3e84d5ea3a9658ef900453bc))
|
||||
* parcel error for nullable types ([336eed3](https://github.com/ReVanced/revanced-manager/commit/336eed3a95111ebbe456321f5986e6875ded354e))
|
||||
* pass worker inputs without serialization ([#44](https://github.com/ReVanced/revanced-manager/issues/44)) ([059a72b](https://github.com/ReVanced/revanced-manager/commit/059a72b9dd9103d2b3704daa7dbb13ad83971460))
|
||||
* patch count remaining at zero when using process runtime ([#2542](https://github.com/ReVanced/revanced-manager/issues/2542)) ([f5e1e0b](https://github.com/ReVanced/revanced-manager/commit/f5e1e0b0659e5775dd460b8dfc15427eb0175139))
|
||||
* patch options reset button being broken ([e1647fd](https://github.com/ReVanced/revanced-manager/commit/e1647fdef0c9f68e171a2d15e2b6e744da6bbaf5))
|
||||
* Patch process cancelation dialog conditions ([#2554](https://github.com/ReVanced/revanced-manager/issues/2554)) ([e97b19d](https://github.com/ReVanced/revanced-manager/commit/e97b19d2b65dbfc49ed062b123c363e412b9bf8e))
|
||||
* Patch selection screen padding ([#2533](https://github.com/ReVanced/revanced-manager/issues/2533)) ([cd2dbcc](https://github.com/ReVanced/revanced-manager/commit/cd2dbcc841e56dac99230ea6501af87c43e9c572))
|
||||
* **patcher:** add notification and wakelock to worker; chore: add app icon ([8b6d32d](https://github.com/ReVanced/revanced-manager/commit/8b6d32dd7b3ca4c694414a55a1b6202b62636530))
|
||||
* patches not being reloaded ([dccf861](https://github.com/ReVanced/revanced-manager/commit/dccf86163af34341e3e451df9f24356c7294ae1e))
|
||||
* **patches selector:** copy the selected patches list ([70e49aa](https://github.com/ReVanced/revanced-manager/commit/70e49aaaa3a42510cb9ced2209c90cd1da98391d))
|
||||
* perform selected app operations in the correct order ([34cf848](https://github.com/ReVanced/revanced-manager/commit/34cf848baaaa2504d162c515a95240d45bd7092a))
|
||||
* permission error when using installed app ([8767f0e](https://github.com/ReVanced/revanced-manager/commit/8767f0e99c6de5bbb0a690ced40f6e9a486f0828))
|
||||
* Playback Switch's Haptic Feedback ([#2639](https://github.com/ReVanced/revanced-manager/issues/2639)) ([9fdca5a](https://github.com/ReVanced/revanced-manager/commit/9fdca5a0afd6be8a24e2ec09eec0000b0b9cd179))
|
||||
* process death resilience and account for android 11 bug ([#2355](https://github.com/ReVanced/revanced-manager/issues/2355)) ([83eeeae](https://github.com/ReVanced/revanced-manager/commit/83eeeae801827800a0787e9e753c72d2a24d7970))
|
||||
* progress bar not updating ([dcaa38c](https://github.com/ReVanced/revanced-manager/commit/dcaa38c8824f54da7a833c354b247f309d1c9871))
|
||||
* release builds not working properly ([6f6476e](https://github.com/ReVanced/revanced-manager/commit/6f6476e85158cad4e2497e9f72b73c4dc948f0bc))
|
||||
* remove battery optimization notification if user grants the permission ([9863c51](https://github.com/ReVanced/revanced-manager/commit/9863c5161a1bc16941a323e654f80f8cb0122f9f))
|
||||
* remove the unique constraint for patch bundle names ([ea29d0f](https://github.com/ReVanced/revanced-manager/commit/ea29d0f00c3b3b2c137c4849e6c445a6bf9a180f))
|
||||
* Remove unnecessary screen padding ([8419f75](https://github.com/ReVanced/revanced-manager/commit/8419f75d597dd198aa1029fae2109646c5874078)), closes [#2062](https://github.com/ReVanced/revanced-manager/issues/2062)
|
||||
* remove unused function preventing compilation ([2297e94](https://github.com/ReVanced/revanced-manager/commit/2297e94cb81a9a22ea032d8e247769774ca85087))
|
||||
* Reset cached theme on theme change to avoid broken colors ([#2527](https://github.com/ReVanced/revanced-manager/issues/2527)) ([9a82b78](https://github.com/ReVanced/revanced-manager/commit/9a82b785280954973cafc5e6dccb3c90fdb5ef49))
|
||||
* run blocking IO operations in the correct context ([969ddb7](https://github.com/ReVanced/revanced-manager/commit/969ddb7bef321d7aa2a682b8128b1f755f35c28b))
|
||||
* run props flow on correct dispatcher ([#2035](https://github.com/ReVanced/revanced-manager/issues/2035)) ([d3d4c27](https://github.com/ReVanced/revanced-manager/commit/d3d4c27f6d7affceef233a0138ee6c985c7f56bc))
|
||||
* Screen turns off while patching due to wrong WakeLock ([#2147](https://github.com/ReVanced/revanced-manager/issues/2147)) ([4de5340](https://github.com/ReVanced/revanced-manager/commit/4de534094adc0665021d3ba129a648d896718568))
|
||||
* scrolling in patch selector ([154f036](https://github.com/ReVanced/revanced-manager/commit/154f036fe956096bca983fe9d6654ccca38fd8ac))
|
||||
* Selected patch count ([#2559](https://github.com/ReVanced/revanced-manager/issues/2559)) ([a91ff60](https://github.com/ReVanced/revanced-manager/commit/a91ff60533b44629ea60e8cd6acceeb80b0253b7))
|
||||
* serialization not working ([4d04ae0](https://github.com/ReVanced/revanced-manager/commit/4d04ae088c406d84936120cb753cd1f11fb8a8c2))
|
||||
* show available and selected patches in patch selector screen ([61f1ee0](https://github.com/ReVanced/revanced-manager/commit/61f1ee0627d6cbb6b9a4d226eb6c2f9e0b8c6453))
|
||||
* show install button when installation has been cancelled ([93f4a5b](https://github.com/ReVanced/revanced-manager/commit/93f4a5bb7c912ca77bb04e414432922c89d3e2c0))
|
||||
* Show selection warning also on patch option ([#2643](https://github.com/ReVanced/revanced-manager/issues/2643)) ([3b82767](https://github.com/ReVanced/revanced-manager/commit/3b82767a897eeca1dda1d8343f1db4207050e960))
|
||||
* sources screen being misaligned during transitions ([2ac3d5c](https://github.com/ReVanced/revanced-manager/commit/2ac3d5c483d5cc4776681ed3f900550a4e45f616))
|
||||
* specify `multithreadingDexFileWriter` in `PatcherOptions` ([#1402](https://github.com/ReVanced/revanced-manager/issues/1402)) ([3f362b6](https://github.com/ReVanced/revanced-manager/commit/3f362b605fbce3ea72e7c95b7e0bc614443c7d44))
|
||||
* Support patching on ARMv7 by updating AAPT2 ([#2084](https://github.com/ReVanced/revanced-manager/issues/2084)) ([15b47f9](https://github.com/ReVanced/revanced-manager/commit/15b47f9bb6cd6bb0360fda6ac641cd4c75542287))
|
||||
* Transparent status on fullscreen dialog ([#2654](https://github.com/ReVanced/revanced-manager/issues/2654)) ([a8820a4](https://github.com/ReVanced/revanced-manager/commit/a8820a4daf71704f6945b8f794495fe8a8d7589e))
|
||||
* Turn off filters by default ([#2079](https://github.com/ReVanced/revanced-manager/issues/2079)) ([44f8b1f](https://github.com/ReVanced/revanced-manager/commit/44f8b1fb6bffed5866ada356910119465320a9a8))
|
||||
* typo in string name `import_keystore_description` ([#1273](https://github.com/ReVanced/revanced-manager/issues/1273)) ([933e69e](https://github.com/ReVanced/revanced-manager/commit/933e69e21e97fede2183a26dd1645a6eb96c4509))
|
||||
* **ui:** make entire patches view button selectable ([#1271](https://github.com/ReVanced/revanced-manager/issues/1271)) ([83cdaae](https://github.com/ReVanced/revanced-manager/commit/83cdaaee183ff1b6d905977df38fe4e47f7d5973))
|
||||
* Updates popup shows incorrect names ([#1283](https://github.com/ReVanced/revanced-manager/issues/1283)) ([c879faf](https://github.com/ReVanced/revanced-manager/commit/c879faf2eb338476c6abd9f104922b0d49f95cd6))
|
||||
* Use `compatible` rather than `support` when referring to patch compatibility ([#2422](https://github.com/ReVanced/revanced-manager/issues/2422)) ([8b3c4eb](https://github.com/ReVanced/revanced-manager/commit/8b3c4eb91c491a0971e2ccf7d46012437eca5c25))
|
||||
* use correct `getViewModel` ([5b6ae80](https://github.com/ReVanced/revanced-manager/commit/5b6ae800fdfc93ef5058b21b3e48daac2a4e1358))
|
||||
* use correct classes to determine option type ([e833bf4](https://github.com/ReVanced/revanced-manager/commit/e833bf4ad14811bb6880ae2d97055e4ce0de222f))
|
||||
* use correct directory ([9e1ebb3](https://github.com/ReVanced/revanced-manager/commit/9e1ebb390244dcb9af03a9164a32386481ec5691))
|
||||
* Use FAB instead of ListItem to patch in App Overview ([6ace71b](https://github.com/ReVanced/revanced-manager/commit/6ace71b739302466274ce9b46f5f7dd6ab9da05d)), closes [#1995](https://github.com/ReVanced/revanced-manager/issues/1995)
|
||||
* use proper update icon ([b59a161](https://github.com/ReVanced/revanced-manager/commit/b59a16191a61c64275137c4a6145fd30d68aa480))
|
||||
* use ReVanced ring logo in about section ([#1302](https://github.com/ReVanced/revanced-manager/issues/1302)) ([933a4a3](https://github.com/ReVanced/revanced-manager/commit/933a4a32203425e745e05615217a8d0975c2e959))
|
||||
* Use the correct icon in API URL dialog ([c22e5b4](https://github.com/ReVanced/revanced-manager/commit/c22e5b4051515e0f02828a2b30f6af19b48ba55f)), closes [#1972](https://github.com/ReVanced/revanced-manager/issues/1972)
|
||||
* use upsert when modifying installed apps ([90edf1d](https://github.com/ReVanced/revanced-manager/commit/90edf1ddd0de29b299855810402a31828d989d04))
|
||||
* **VersionSelector:** use correct LazyColumn item key ([413fe98](https://github.com/ReVanced/revanced-manager/commit/413fe980a8c0b45e3924c98b2fbd1a3e9b579528))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **about screen:** complete about screen ([1d6b34a](https://github.com/ReVanced/revanced-manager/commit/1d6b34a39f76e8e733649f7fcfeb20eb1009a39a))
|
||||
* Add `isScrollingUp` support for ScrollState ([bf049c3](https://github.com/ReVanced/revanced-manager/commit/bf049c3c1ac12a60c5c6226b5c3fec7f72caa7db))
|
||||
* add ability to share debug logs ([feb0ca4](https://github.com/ReVanced/revanced-manager/commit/feb0ca4cf315e5d332f36039fbb989b3cfb9cf58))
|
||||
* add checkboxes to the downloaded apps page ([ca93524](https://github.com/ReVanced/revanced-manager/commit/ca93524be0b37f38b860d8512c81d2898b2860af))
|
||||
* Add confirm dialogs when toggling dangerous settings ([#2072](https://github.com/ReVanced/revanced-manager/issues/2072)) ([6643276](https://github.com/ReVanced/revanced-manager/commit/66432764cfe8192f4cf8e599a592f27c675f25ec))
|
||||
* Add confirmation dialog to "Reset" options ([#2576](https://github.com/ReVanced/revanced-manager/issues/2576)) ([f32ffbb](https://github.com/ReVanced/revanced-manager/commit/f32ffbb6f2224f886af14205721fb2372f396de2))
|
||||
* Add downloader plugin system ([#2041](https://github.com/ReVanced/revanced-manager/issues/2041)) ([ca38737](https://github.com/ReVanced/revanced-manager/commit/ca3873778307612b93af3273ffe4821c6a5e398d))
|
||||
* add external process runtime ([#1799](https://github.com/ReVanced/revanced-manager/issues/1799)) ([0d73e0c](https://github.com/ReVanced/revanced-manager/commit/0d73e0cd32b6af3526c226ce4695c7e905f65b15))
|
||||
* Add haptic feedback ([#1457](https://github.com/ReVanced/revanced-manager/issues/1457)) ([76e0c95](https://github.com/ReVanced/revanced-manager/commit/76e0c9518746620cd2723a99c310f92f5b3fd996))
|
||||
* Add installer status dialog ([#1473](https://github.com/ReVanced/revanced-manager/issues/1473)) ([43b3743](https://github.com/ReVanced/revanced-manager/commit/43b37432138d7cd8a507efad80827d6f3bdcdf08))
|
||||
* add network checks for features that require it ([f3f8bc4](https://github.com/ReVanced/revanced-manager/commit/f3f8bc4ec2f593ade91324d78f9ce83f60ef65cc))
|
||||
* add patch bundle info screen ([#55](https://github.com/ReVanced/revanced-manager/issues/55)) ([8ae4e85](https://github.com/ReVanced/revanced-manager/commit/8ae4e850dae9cf4df14afe90048ca0b0a48389ac))
|
||||
* add patches selector bottom sheet ([#1360](https://github.com/ReVanced/revanced-manager/issues/1360)) ([f6fb534](https://github.com/ReVanced/revanced-manager/commit/f6fb534e04777b4f0ec2ff2b13768c724c68c028))
|
||||
* add required options screen ([#2378](https://github.com/ReVanced/revanced-manager/issues/2378)) ([3a63e42](https://github.com/ReVanced/revanced-manager/commit/3a63e42df9ce50069a573d98cf44a8abec03b639))
|
||||
* Add reset button to custom API ([#2076](https://github.com/ReVanced/revanced-manager/issues/2076)) ([df52a7b](https://github.com/ReVanced/revanced-manager/commit/df52a7bdef05e1c9f034ae067c3dd183fb8fdffd)), closes [#2051](https://github.com/ReVanced/revanced-manager/issues/2051)
|
||||
* Add sensitivity to `isScrollingUp` ([f6ca4e9](https://github.com/ReVanced/revanced-manager/commit/f6ca4e95551193c8d21afd09872d9bbe6c80c0e8))
|
||||
* add social links ([#1294](https://github.com/ReVanced/revanced-manager/issues/1294)) ([7df3350](https://github.com/ReVanced/revanced-manager/commit/7df3350acb4aae957e2a7c0d2f30faf6cae6ab85))
|
||||
* add toast feedback to the bundle update button ([ea50e65](https://github.com/ReVanced/revanced-manager/commit/ea50e65ab1d626152bdd40c1893cd408b7271472))
|
||||
* add user agent ([#1382](https://github.com/ReVanced/revanced-manager/issues/1382)) ([3aea6cb](https://github.com/ReVanced/revanced-manager/commit/3aea6cbaecc9db103e9a3925b3c4a531de6c5f0e))
|
||||
* advanced settings page with device info ([#51](https://github.com/ReVanced/revanced-manager/issues/51)) ([86e4244](https://github.com/ReVanced/revanced-manager/commit/86e42449eb553417726b95f79f6edd7f526f6d44))
|
||||
* allow bundles to use classes from other bundles ([#1951](https://github.com/ReVanced/revanced-manager/issues/1951)) ([af8e2b4](https://github.com/ReVanced/revanced-manager/commit/af8e2b44c027d978046a0e7926f1425f0348b098))
|
||||
* allow user to save logs ([a008cf5](https://github.com/ReVanced/revanced-manager/commit/a008cf5dd143fafb1f642cd037db29393716f7d5))
|
||||
* animate the arrow button ([db070b1](https://github.com/ReVanced/revanced-manager/commit/db070b125bf08ff251450259045755e6469c2d5e))
|
||||
* app downloader ([#43](https://github.com/ReVanced/revanced-manager/issues/43)) ([1f1a480](https://github.com/ReVanced/revanced-manager/commit/1f1a480d51edb310934523024c52e0c19b066662))
|
||||
* app selector screen ([373cc4b](https://github.com/ReVanced/revanced-manager/commit/373cc4bbb1a8194bf9475d0a13e1c154cd87480b))
|
||||
* **app-selector:** show patchable installed apps first ([#1496](https://github.com/ReVanced/revanced-manager/issues/1496)) ([afb0f80](https://github.com/ReVanced/revanced-manager/commit/afb0f80de5a73c213f77bfde761ea1ea0886abef))
|
||||
* armv7 warning ([2ffcaec](https://github.com/ReVanced/revanced-manager/commit/2ffcaec724d5a13b816e04813d45cde75681eb69))
|
||||
* Automatic language detection ([#2032](https://github.com/ReVanced/revanced-manager/issues/2032)) ([36a1c3f](https://github.com/ReVanced/revanced-manager/commit/36a1c3f36807500fbe820bf4142fef159b138c7d))
|
||||
* backend ([45a54d1](https://github.com/ReVanced/revanced-manager/commit/45a54d1608a77547e06748867d63a452224727b6))
|
||||
* better installer ui ([#29](https://github.com/ReVanced/revanced-manager/issues/29)) ([14888f9](https://github.com/ReVanced/revanced-manager/commit/14888f9da71ecf1c50d770123d1e8dd09aa6c8b1))
|
||||
* **bundles tab:** add BackHandler ([a9171e1](https://github.com/ReVanced/revanced-manager/commit/a9171e17bd628601f1e074a7fcdf74c15cb73709))
|
||||
* Change "Update" to "Show" in Update Available notification ([5c43413](https://github.com/ReVanced/revanced-manager/commit/5c434137d332aabaaca236b6f9616d7727d0b3d2)), closes [#1959](https://github.com/ReVanced/revanced-manager/issues/1959)
|
||||
* change appID and name of debug builds ([5b3e9e5](https://github.com/ReVanced/revanced-manager/commit/5b3e9e595cded277c051cc669d9f29bcb6ce5d18))
|
||||
* **Changelogs:** overall improvement ([#1429](https://github.com/ReVanced/revanced-manager/issues/1429)) ([2a3590d](https://github.com/ReVanced/revanced-manager/commit/2a3590ddd2cc74b746a3f632a93970bfa23cf384))
|
||||
* check for updates on startup ([#1462](https://github.com/ReVanced/revanced-manager/issues/1462)) ([bb2164e](https://github.com/ReVanced/revanced-manager/commit/bb2164e1a95a698b1b0f69e725af5e0e1e45b868))
|
||||
* check if the version being used is the recommended version ([#1675](https://github.com/ReVanced/revanced-manager/issues/1675)) ([9d961f6](https://github.com/ReVanced/revanced-manager/commit/9d961f6a52d15ed6116afc78c7008460347da69a))
|
||||
* Collapse ExtendedFAB on scroll ([#1630](https://github.com/ReVanced/revanced-manager/issues/1630)) ([b5c1f6d](https://github.com/ReVanced/revanced-manager/commit/b5c1f6d732b65c1c9becb7962c51a70a840dea73))
|
||||
* **Compose:** Add confirmation dialog on multiple operations ([#2529](https://github.com/ReVanced/revanced-manager/issues/2529)) ([2671e68](https://github.com/ReVanced/revanced-manager/commit/2671e68004269deebdedaee38a6692b2302ca732))
|
||||
* **Compose:** hide developer settings ([#2551](https://github.com/ReVanced/revanced-manager/issues/2551)) ([0030c7a](https://github.com/ReVanced/revanced-manager/commit/0030c7a7885feee0578ee1423ee2aefc6a0e2c2c))
|
||||
* **Compose:** Improve patches selector tab by adding the bundle version ([#2545](https://github.com/ReVanced/revanced-manager/issues/2545)) ([3710675](https://github.com/ReVanced/revanced-manager/commit/3710675ac0ca77cecfb172b4cf148f41a762bf06))
|
||||
* **Compose:** Move developer options to top level ([#2528](https://github.com/ReVanced/revanced-manager/issues/2528)) ([cedc6ad](https://github.com/ReVanced/revanced-manager/commit/cedc6ad49f23d778a52a8846f9e384fd2106e074))
|
||||
* contributors screen ([#42](https://github.com/ReVanced/revanced-manager/issues/42)) ([3f54381](https://github.com/ReVanced/revanced-manager/commit/3f54381d307fd71296be18e97a1ab870f1cdc297))
|
||||
* **Contributors Screen:** implement design from Figma ([#1465](https://github.com/ReVanced/revanced-manager/issues/1465)) ([d5bdc29](https://github.com/ReVanced/revanced-manager/commit/d5bdc293f308e2a283d744afdc1aed6a165f7166))
|
||||
* Dashboard Screen ([#18](https://github.com/ReVanced/revanced-manager/issues/18)) ([a127b95](https://github.com/ReVanced/revanced-manager/commit/a127b959ead5a9c83a0c4f7e7840aeeb68362c0d))
|
||||
* disable filter chips when there are no patches ([fd520bb](https://github.com/ReVanced/revanced-manager/commit/fd520bba700bae9d8eae745ce23a95b07b7f7d34))
|
||||
* dont ask for root on launch ([9562d80](https://github.com/ReVanced/revanced-manager/commit/9562d80bfdc785fe5ed512a15cfd7c0e09091acc))
|
||||
* download apps in patcher screen ([#73](https://github.com/ReVanced/revanced-manager/issues/73)) ([a854221](https://github.com/ReVanced/revanced-manager/commit/a854221969c363712a0b3de84607092709db291f))
|
||||
* experimental patches setting ([b07fd23](https://github.com/ReVanced/revanced-manager/commit/b07fd2321dd0aecce556f341e2b18f930baa58fd))
|
||||
* filter options for patches ([62bccd1](https://github.com/ReVanced/revanced-manager/commit/62bccd150441747e5cd6de71de304e416922bdda))
|
||||
* finish implementing the sources system ([#70](https://github.com/ReVanced/revanced-manager/issues/70)) ([858b0ec](https://github.com/ReVanced/revanced-manager/commit/858b0ec5b456043fa61b681bbbd195fd9c30a6f0))
|
||||
* get bundle information from jar manifest ([#2027](https://github.com/ReVanced/revanced-manager/issues/2027)) ([60fdec9](https://github.com/ReVanced/revanced-manager/commit/60fdec9804c763ef9308a7a56d245401dbd35d7c))
|
||||
* hide tabs when 1 bundle is used ([41268ca](https://github.com/ReVanced/revanced-manager/commit/41268ca80b71f68dbf9523fa7bac34feeec7d011))
|
||||
* hide unfinished pages in release mode ([c199801](https://github.com/ReVanced/revanced-manager/commit/c199801fb7f91306538391177d240cf1121964d2))
|
||||
* Highlight links in Markdown ([7bf8988](https://github.com/ReVanced/revanced-manager/commit/7bf89887e420a402b30da4796ba3648147f00394)), closes [#1962](https://github.com/ReVanced/revanced-manager/issues/1962)
|
||||
* implement DI ([7fa7b9d](https://github.com/ReVanced/revanced-manager/commit/7fa7b9d53a3217c7e1e4c70a524fd68ae170c832))
|
||||
* implement more patch option types ([#2015](https://github.com/ReVanced/revanced-manager/issues/2015)) ([b18c678](https://github.com/ReVanced/revanced-manager/commit/b18c6783547e910fa2dbd3d7edcc5fe329e6d921))
|
||||
* implement navigation ([7fc6ec5](https://github.com/ReVanced/revanced-manager/commit/7fc6ec5c2cf8eb9ebfc3dda01cdfd80962be1f8f))
|
||||
* implement Submit Issue button ([#1276](https://github.com/ReVanced/revanced-manager/issues/1276)) ([a269a39](https://github.com/ReVanced/revanced-manager/commit/a269a39aa4a34b94aef4e1e85126c571e96be575))
|
||||
* improve accessibility ([#64](https://github.com/ReVanced/revanced-manager/issues/64)) ([39b08e5](https://github.com/ReVanced/revanced-manager/commit/39b08e5201d2cec6bdb67f9386120a7a40c9ccc6))
|
||||
* Improve APK file name formatting on save ([#2421](https://github.com/ReVanced/revanced-manager/issues/2421)) ([a53a8ba](https://github.com/ReVanced/revanced-manager/commit/a53a8ba62734daf9bd80ab79265241a4a22f489c))
|
||||
* improve bundle dialog UI ([409c888](https://github.com/ReVanced/revanced-manager/commit/409c888d523f398505daaaff9d2490dc5a863680))
|
||||
* Improve bundle info screen design ([#2548](https://github.com/ReVanced/revanced-manager/issues/2548)) ([55524f7](https://github.com/ReVanced/revanced-manager/commit/55524f7284a44bbf8e8c782eedd7fc06d54944cf))
|
||||
* Improve custom API URL dialog ([#2033](https://github.com/ReVanced/revanced-manager/issues/2033)) ([7dae562](https://github.com/ReVanced/revanced-manager/commit/7dae56281994942577bac7bf50c59e805672d0e1))
|
||||
* Improve device information in debugging section ([d889677](https://github.com/ReVanced/revanced-manager/commit/d889677b29aeb4a49a025da98060265e88876ddf)), closes [#1977](https://github.com/ReVanced/revanced-manager/issues/1977)
|
||||
* Improve initial update popup wording ([5901372](https://github.com/ReVanced/revanced-manager/commit/5901372523643eef5a605256662c8e1f0a9f2263)), closes [#1956](https://github.com/ReVanced/revanced-manager/issues/1956)
|
||||
* improve keystore UI and UX ([#52](https://github.com/ReVanced/revanced-manager/issues/52)) ([49b4bbb](https://github.com/ReVanced/revanced-manager/commit/49b4bbbf0ba84b006a1694ca95662cf224a84b0f))
|
||||
* Improve patch bundle screen ([#2070](https://github.com/ReVanced/revanced-manager/issues/2070)) ([a907528](https://github.com/ReVanced/revanced-manager/commit/a907528a2096d8de9778efa8f85e0cdc1d7c2b80))
|
||||
* improve patcher screen labels ([f4d6c60](https://github.com/ReVanced/revanced-manager/commit/f4d6c60b9ec4c76e8e3fa233f79e062b802860e5))
|
||||
* improve patcher UI ([#1494](https://github.com/ReVanced/revanced-manager/issues/1494)) ([429b428](https://github.com/ReVanced/revanced-manager/commit/429b428f673dd949289baaf27ed2e08970db83ae))
|
||||
* Improve Settings order ([#2060](https://github.com/ReVanced/revanced-manager/issues/2060)) ([fa86c1a](https://github.com/ReVanced/revanced-manager/commit/fa86c1a0bb039a86e0649eae30c7b33620f98dbe))
|
||||
* improve the safeguards ([#2038](https://github.com/ReVanced/revanced-manager/issues/2038)) ([e5b414e](https://github.com/ReVanced/revanced-manager/commit/e5b414e277341967c7b5a5f071ddac1fdfdb8e63))
|
||||
* Improve unsupported patch warnings ([#2066](https://github.com/ReVanced/revanced-manager/issues/2066)) ([3c23d57](https://github.com/ReVanced/revanced-manager/commit/3c23d573bf3998304cad4485016004a871cf1636)), closes [#2052](https://github.com/ReVanced/revanced-manager/issues/2052)
|
||||
* Improve update screen design ([#2487](https://github.com/ReVanced/revanced-manager/issues/2487)) ([7007010](https://github.com/ReVanced/revanced-manager/commit/7007010f14239452e565736fe7cee7666a682ffb))
|
||||
* Improve update setting tile titles ([e2623d6](https://github.com/ReVanced/revanced-manager/commit/e2623d6d79b3b87e9ba29016e42f1d645b2f9e19)), closes [#1968](https://github.com/ReVanced/revanced-manager/issues/1968)
|
||||
* improve UX for failed or missing bundles ([49f8510](https://github.com/ReVanced/revanced-manager/commit/49f851022db72b110c8597aa1c711461c1b01882))
|
||||
* improved compose stability ([8c40119](https://github.com/ReVanced/revanced-manager/commit/8c40119609c650d1f012d810a4117e84fbe2da52))
|
||||
* improved dashboard screen ([5c2f9d9](https://github.com/ReVanced/revanced-manager/commit/5c2f9d91a6e803d9b3705e2b3aa84176353ba963))
|
||||
* in-app updater ([#25](https://github.com/ReVanced/revanced-manager/issues/25)) ([d71a4bf](https://github.com/ReVanced/revanced-manager/commit/d71a4bf3c3457a02578bb8ad3c7615b074f6e3f1))
|
||||
* **installer:** adjust arrow icon size ([e997255](https://github.com/ReVanced/revanced-manager/commit/e997255cf3c3c5ba777da07752217f99e01dd789))
|
||||
* **installer:** adjust step icon size and alignment ([cfcabf6](https://github.com/ReVanced/revanced-manager/commit/cfcabf6ef1c212f2627d5d02f4d59981bdc276ca))
|
||||
* **installer:** apk signing and installation ([da32ff9](https://github.com/ReVanced/revanced-manager/commit/da32ff954a84cf8ff321bbbf71cc5b544d6e6be9))
|
||||
* **installer:** sign apk in patcher worker ([c003c3c](https://github.com/ReVanced/revanced-manager/commit/c003c3c3245f5a663a0371d4e9df71777ba728b9))
|
||||
* **Installer:** use BottomAppBar ([#1428](https://github.com/ReVanced/revanced-manager/issues/1428)) ([ceb7623](https://github.com/ReVanced/revanced-manager/commit/ceb762379461443e7e62c37511df1c84a6068bb4))
|
||||
* integrate revanced patcher ([#22](https://github.com/ReVanced/revanced-manager/issues/22)) ([caeabfc](https://github.com/ReVanced/revanced-manager/commit/caeabfc91b2aa7e3de9e6a31859049d4b2d37388))
|
||||
* keystore import/export ([#30](https://github.com/ReVanced/revanced-manager/issues/30)) ([fd0ec6c](https://github.com/ReVanced/revanced-manager/commit/fd0ec6c6a7fc8488db859056a95ebe0455e2843b))
|
||||
* **koin:** use the android logger ([f30333e](https://github.com/ReVanced/revanced-manager/commit/f30333e75338dd2c1ef891723ecb834fc1eb10f7))
|
||||
* licenses screen ([#47](https://github.com/ReVanced/revanced-manager/issues/47)) ([e3cb056](https://github.com/ReVanced/revanced-manager/commit/e3cb056858ea8917162c1a421a7a8d03ddaa08e2))
|
||||
* make bundles selectable ([#1237](https://github.com/ReVanced/revanced-manager/issues/1237)) ([a246863](https://github.com/ReVanced/revanced-manager/commit/a246863a89fe8781feaf2a45fcb7ea991d26028f))
|
||||
* Make patch bundles list scrollable ([#2322](https://github.com/ReVanced/revanced-manager/issues/2322)) ([a5c8a23](https://github.com/ReVanced/revanced-manager/commit/a5c8a23f9ffb36543d45b46bb5f01c5dea56bf90))
|
||||
* more info for the select from application screen ([#81](https://github.com/ReVanced/revanced-manager/issues/81)) ([3f446f8](https://github.com/ReVanced/revanced-manager/commit/3f446f8236101755a9d51a2aa759f70a0bd429da))
|
||||
* move plugin api to another repository ([55e7ebf](https://github.com/ReVanced/revanced-manager/commit/55e7ebf4fc5adf8800430ad4aa2579cb6210290d))
|
||||
* Move safeguards above patcher preference group ([9f7eaa2](https://github.com/ReVanced/revanced-manager/commit/9f7eaa212339f2093050087dc7ab0b8237356939))
|
||||
* move update to notification card ([#1917](https://github.com/ReVanced/revanced-manager/issues/1917)) ([b80f94b](https://github.com/ReVanced/revanced-manager/commit/b80f94b77bba89e31608cdb302dab0619bf7c5cc))
|
||||
* **NotificationCard:** rewrite & consistent usage ([#1426](https://github.com/ReVanced/revanced-manager/issues/1426)) ([f8aafa0](https://github.com/ReVanced/revanced-manager/commit/f8aafa050328423b3168a7943f566fce58100cb0))
|
||||
* Open the app-specific manage all files permission dialog ([#2148](https://github.com/ReVanced/revanced-manager/issues/2148)) ([a3f31ea](https://github.com/ReVanced/revanced-manager/commit/a3f31ea65788a43ce57d548e8240e5b1fe3005d0))
|
||||
* Order bundles by number of patches ([bb5d414](https://github.com/ReVanced/revanced-manager/commit/bb5d414abb4f294aa88d795486836a99ade2b388))
|
||||
* patch bundle sources system ([#24](https://github.com/ReVanced/revanced-manager/issues/24)) ([9675a27](https://github.com/ReVanced/revanced-manager/commit/9675a2777b364e5ede0d44b92eb7e551d4f7b3d6))
|
||||
* patch options ([#45](https://github.com/ReVanced/revanced-manager/issues/45)) ([8540d30](https://github.com/ReVanced/revanced-manager/commit/8540d301962669e3d79ca345c852f5b01df641a4))
|
||||
* patch options UI ([#80](https://github.com/ReVanced/revanced-manager/issues/80)) ([0a1acd2](https://github.com/ReVanced/revanced-manager/commit/0a1acd24e3f0d06fde412b8eeecd923d92ee64a9))
|
||||
* **patch-selector:** default patches selection ([#1272](https://github.com/ReVanced/revanced-manager/issues/1272)) ([a17c2de](https://github.com/ReVanced/revanced-manager/commit/a17c2de228cccb4a0bb0ca7497720011bec131fc))
|
||||
* **patch-selector:** remove TODO about an unplanned feature ([4924eae](https://github.com/ReVanced/revanced-manager/commit/4924eaef800c429f2a59b8a15fd48fae0292810c))
|
||||
* **patcher:** Improve installation ([#2185](https://github.com/ReVanced/revanced-manager/issues/2185)) ([3bd4f0d](https://github.com/ReVanced/revanced-manager/commit/3bd4f0d8f3f60d079d4647d42592b10a15f0dae8))
|
||||
* patches selector screen ([55e871a](https://github.com/ReVanced/revanced-manager/commit/55e871aa7d27885e44ef33faab1bb4ae33e7a460))
|
||||
* Progressive AlertDialog for adding bundles ([9a01273](https://github.com/ReVanced/revanced-manager/commit/9a01273c43bd6bcdb0cdfd26c5a467cd3193e5d7)), closes [#1992](https://github.com/ReVanced/revanced-manager/issues/1992)
|
||||
* ProGuard ([d84e6a3](https://github.com/ReVanced/revanced-manager/commit/d84e6a3ffc20d018b2edeb505de20a920785ba5c))
|
||||
* Purple default theme ([#1601](https://github.com/ReVanced/revanced-manager/issues/1601)) ([0616666](https://github.com/ReVanced/revanced-manager/commit/0616666d5ef9b53bef5fd630b1b1a47088097d37))
|
||||
* Redesign the patches screen ([#2381](https://github.com/ReVanced/revanced-manager/issues/2381)) ([8dc4e5b](https://github.com/ReVanced/revanced-manager/commit/8dc4e5b89ee4d36263c8b4187650691b68484688))
|
||||
* remember patch options ([#1449](https://github.com/ReVanced/revanced-manager/issues/1449)) ([90db765](https://github.com/ReVanced/revanced-manager/commit/90db765c9aa014495775a34927904dedf5fef1e3))
|
||||
* remove dead help icons ([3bb071d](https://github.com/ReVanced/revanced-manager/commit/3bb071d80d319d4943b0d4c3048f232f3eb9f5cf))
|
||||
* Remove tag from changelog ([d2119d3](https://github.com/ReVanced/revanced-manager/commit/d2119d36430198151140b469192f76f781df6dd3))
|
||||
* Rename "Patch bundle" to "Patches" ([#2541](https://github.com/ReVanced/revanced-manager/issues/2541)) ([2cdd6d1](https://github.com/ReVanced/revanced-manager/commit/2cdd6d1843f1e49c7c720f8859e11d6a30c0eea6))
|
||||
* rename debug build to `ReVanced Manager (dev)` ([d3417ad](https://github.com/ReVanced/revanced-manager/commit/d3417adbeba0a8e06d3494a2fd108f735f73632c))
|
||||
* rename main bundle to `Default` ([e44d3fd](https://github.com/ReVanced/revanced-manager/commit/e44d3fdee444d915e3e8b8143e55f1353980aad2))
|
||||
* rename package to `app.revanced.manager` ([5ec97f4](https://github.com/ReVanced/revanced-manager/commit/5ec97f4a852a07d0e554bbe1eacc379179ac089e))
|
||||
* Rename strings ([e127845](https://github.com/ReVanced/revanced-manager/commit/e1278452b9c73479cdfb0eb0703db1552b158633))
|
||||
* rename ViewModels for consistency ([064a54e](https://github.com/ReVanced/revanced-manager/commit/064a54eaf0675a1cc9d21f3e1071160deb25c201))
|
||||
* Reorder Import & Export settings ([#2403](https://github.com/ReVanced/revanced-manager/issues/2403)) ([2697077](https://github.com/ReVanced/revanced-manager/commit/2697077fc88bb795027303558c9d52448a4daded))
|
||||
* ReVanced theme colors ([59b894d](https://github.com/ReVanced/revanced-manager/commit/59b894dce4b99c51151a4cccd03a998ceec31778))
|
||||
* revert to blue theme colors ([5f4c958](https://github.com/ReVanced/revanced-manager/commit/5f4c9584a94a1edd1eeaa0b9ecfcd9b281b7cccc))
|
||||
* root installation ([#1243](https://github.com/ReVanced/revanced-manager/issues/1243)) ([62e934c](https://github.com/ReVanced/revanced-manager/commit/62e934c4032096bed36201510fc55304ba48de68))
|
||||
* save patch options and selected patches in bundle ([#50](https://github.com/ReVanced/revanced-manager/issues/50)) ([23162f6](https://github.com/ReVanced/revanced-manager/commit/23162f6233fa6a176514b35feff731f8f28b4d4b))
|
||||
* save patch selection using room db ([#38](https://github.com/ReVanced/revanced-manager/issues/38)) ([1efccda](https://github.com/ReVanced/revanced-manager/commit/1efccda3f55d964fae3bee9ee1f0bd260bb1cc74))
|
||||
* Screen slide transition ([#2396](https://github.com/ReVanced/revanced-manager/issues/2396)) ([2de16e1](https://github.com/ReVanced/revanced-manager/commit/2de16e18e8ba5e84149b377f225693ea35fa2385))
|
||||
* Scrollbars ([#1479](https://github.com/ReVanced/revanced-manager/issues/1479)) ([b5558ea](https://github.com/ReVanced/revanced-manager/commit/b5558ea3ffef40f96b271f8dfe3a5cf95328781e))
|
||||
* Select bundle type before adding bundle ([#1490](https://github.com/ReVanced/revanced-manager/issues/1490)) ([88e860c](https://github.com/ReVanced/revanced-manager/commit/88e860cf0132aed23a3cfd3d9d12e472aa895718))
|
||||
* selected app info page ([#1395](https://github.com/ReVanced/revanced-manager/issues/1395)) ([b69a369](https://github.com/ReVanced/revanced-manager/commit/b69a369d4e304c8a4c8a8db052309b485171e353))
|
||||
* Set app ownership when installing apps ([#2558](https://github.com/ReVanced/revanced-manager/issues/2558)) ([7c410fe](https://github.com/ReVanced/revanced-manager/commit/7c410fef4512087657e3978d5be049c422b25456))
|
||||
* settings migration (compose) ([#1309](https://github.com/ReVanced/revanced-manager/issues/1309)) ([bf1d628](https://github.com/ReVanced/revanced-manager/commit/bf1d628944cb5a439d0bda7c49d820a5fa7576b3))
|
||||
* settings screen ([b7d53cf](https://github.com/ReVanced/revanced-manager/commit/b7d53cfca84d7239bed9189e265a03fd44dc2e45))
|
||||
* **settings screen:** add battery optimization notification ([5754864](https://github.com/ReVanced/revanced-manager/commit/57548641e7ecd06decfc926cb860674ce7443d7a))
|
||||
* **settings screen:** match typography from figma ([948a6d1](https://github.com/ReVanced/revanced-manager/commit/948a6d14404e067907c9e84576cfeba76134aaf6))
|
||||
* **settings:** move experimental patches option to advanced ([805d440](https://github.com/ReVanced/revanced-manager/commit/805d440450d821a26d3ef90a4f97cd796635057d))
|
||||
* **Settings:** use SettingsListItem consistently and overall improvements ([#1427](https://github.com/ReVanced/revanced-manager/issues/1427)) ([5e35893](https://github.com/ReVanced/revanced-manager/commit/5e35893883fa109d74b028478e60b51f97a2e12d))
|
||||
* show installed app in version selector ([1ab1e46](https://github.com/ReVanced/revanced-manager/commit/1ab1e4682ffbfe16c02c438ad833adbfdec58b33))
|
||||
* Show manager update dialog ([#2069](https://github.com/ReVanced/revanced-manager/issues/2069)) ([113a74d](https://github.com/ReVanced/revanced-manager/commit/113a74d270c1c222d4d06049b4edda8f27724a20)), closes [#1963](https://github.com/ReVanced/revanced-manager/issues/1963) [#1958](https://github.com/ReVanced/revanced-manager/issues/1958)
|
||||
* show stacktrace in installer ui ([#36](https://github.com/ReVanced/revanced-manager/issues/36)) ([8d53180](https://github.com/ReVanced/revanced-manager/commit/8d53180d86e6e9d9c8a4056a5fde0603f17e3157))
|
||||
* show toast when no patches are selected ([8aa70d3](https://github.com/ReVanced/revanced-manager/commit/8aa70d350e07aae8b4a22b6bc6fb90c0f6227acd))
|
||||
* splash screen ([60a5a11](https://github.com/ReVanced/revanced-manager/commit/60a5a11c71634aeda414c2ed85f7706ba3deefe1))
|
||||
* store patched apps ([#79](https://github.com/ReVanced/revanced-manager/issues/79)) ([b14285b](https://github.com/ReVanced/revanced-manager/commit/b14285b2c83e60376ad42fa6ea508257cd04d47d))
|
||||
* switch to androidx.navigation ([#2362](https://github.com/ReVanced/revanced-manager/issues/2362)) ([7438f45](https://github.com/ReVanced/revanced-manager/commit/7438f45903ec6ed3436a895d4c32d34d41b00010))
|
||||
* switch to Preferences DataStore ([#60](https://github.com/ReVanced/revanced-manager/issues/60)) ([1852799](https://github.com/ReVanced/revanced-manager/commit/18527999b5f8752faf36c145276d51e2e095c8ee))
|
||||
* switch to revanced api v4 ([7e858a2](https://github.com/ReVanced/revanced-manager/commit/7e858a244cc4038bdb029c4418278700f6a6490f))
|
||||
* switch to the new api ([#75](https://github.com/ReVanced/revanced-manager/issues/75)) ([a55160e](https://github.com/ReVanced/revanced-manager/commit/a55160e7c619ec5541de72fa80f079c9bc94d2d5))
|
||||
* TopAppBar scroll behavior ([#2397](https://github.com/ReVanced/revanced-manager/issues/2397)) ([dc51d61](https://github.com/ReVanced/revanced-manager/commit/dc51d6134dae0fdc415f66e2716c6bffa35dfdb5))
|
||||
* **Update Screen:** changelogs & handle states ([#1464](https://github.com/ReVanced/revanced-manager/issues/1464)) ([3af26e7](https://github.com/ReVanced/revanced-manager/commit/3af26e706571339a3c69688098a51616549c58a8))
|
||||
* **update screen:** complete main update screen ([553af83](https://github.com/ReVanced/revanced-manager/commit/553af831393d7276088ceb0b0a854ec654f72def))
|
||||
* updater changelogs ([#48](https://github.com/ReVanced/revanced-manager/issues/48)) ([6dbcd62](https://github.com/ReVanced/revanced-manager/commit/6dbcd6293e94d8d20cccc401b0edeb1d7047553e))
|
||||
* updater UI and code improvements ([#1597](https://github.com/ReVanced/revanced-manager/issues/1597)) ([a12cae7](https://github.com/ReVanced/revanced-manager/commit/a12cae72998d85138dcf29c0e5d430359e338d5e))
|
||||
* Use "Debug" and "Debug signed" for build names respectively ([5133f02](https://github.com/ReVanced/revanced-manager/commit/5133f02ad61b85af28608c7180b7a2accb4811ab))
|
||||
* Use correct casing in module description ([59b4c0b](https://github.com/ReVanced/revanced-manager/commit/59b4c0b2d2e426dfe66b5a01d219b57bb0df5b8b))
|
||||
* use revanced api for changelogs ([686eb40](https://github.com/ReVanced/revanced-manager/commit/686eb40cb0f8b8d785732dd2bc82d17b5a4fd042))
|
||||
* Use simpler strings ([83d33e8](https://github.com/ReVanced/revanced-manager/commit/83d33e87e3f89cb3efce63dcabcde6478f69b8e7))
|
||||
* View bundle patches ([#2065](https://github.com/ReVanced/revanced-manager/issues/2065)) ([089f200](https://github.com/ReVanced/revanced-manager/commit/089f200fe6ff59020a87883a47ef20a0c4c08565))
|
||||
|
||||
|
||||
### Reverts
|
||||
|
||||
* downgrade Kotlin to 1.8.21 ([fc90bbc](https://github.com/ReVanced/revanced-manager/commit/fc90bbc27ce765e0b55bb5ac9132e58f46aee9aa))
|
||||
@@ -1,4 +1,3 @@
|
||||
import io.github.z4kn4fein.semver.toVersion
|
||||
import kotlin.random.Random
|
||||
|
||||
plugins {
|
||||
@@ -9,12 +8,109 @@ plugins {
|
||||
alias(libs.plugins.compose.compiler)
|
||||
alias(libs.plugins.devtools)
|
||||
alias(libs.plugins.about.libraries)
|
||||
signing
|
||||
}
|
||||
|
||||
val outputApkFileName = "${rootProject.name}-$version.apk"
|
||||
android {
|
||||
namespace = "app.revanced.manager"
|
||||
compileSdk = 35
|
||||
buildToolsVersion = "35.0.1"
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "app.revanced.manager"
|
||||
minSdk = 26
|
||||
targetSdk = 35
|
||||
versionCode = 1
|
||||
versionName = "0.0.1"
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
applicationIdSuffix = ".debug"
|
||||
resValue("string", "app_name", "ReVanced Manager (dev)")
|
||||
isPseudoLocalesEnabled = true
|
||||
|
||||
buildConfigField("long", "BUILD_ID", "${Random.nextLong()}L")
|
||||
}
|
||||
|
||||
release {
|
||||
if (!project.hasProperty("noProguard")) {
|
||||
isMinifyEnabled = true
|
||||
isShrinkResources = true
|
||||
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
||||
}
|
||||
|
||||
if (project.hasProperty("signAsDebug")) {
|
||||
applicationIdSuffix = ".debug"
|
||||
resValue("string", "app_name", "ReVanced Manager Debug")
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
|
||||
isPseudoLocalesEnabled = true
|
||||
}
|
||||
|
||||
buildConfigField("long", "BUILD_ID", "0L")
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
dependenciesInfo {
|
||||
includeInApk = false
|
||||
includeInBundle = false
|
||||
}
|
||||
|
||||
packaging {
|
||||
resources.excludes.addAll(listOf(
|
||||
"/prebuilt/**",
|
||||
"META-INF/DEPENDENCIES",
|
||||
"META-INF/**.version",
|
||||
"DebugProbesKt.bin",
|
||||
"kotlin-tooling-metadata.json",
|
||||
"org/bouncycastle/pqc/**.properties",
|
||||
"org/bouncycastle/x509/**.properties",
|
||||
))
|
||||
jniLibs {
|
||||
useLegacyPackaging = true
|
||||
}
|
||||
}
|
||||
|
||||
ksp {
|
||||
arg("room.schemaLocation", "$projectDir/schemas")
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "17"
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
compose = true
|
||||
aidl = true
|
||||
buildConfig = true
|
||||
}
|
||||
|
||||
android {
|
||||
androidResources {
|
||||
generateLocaleConfig = true
|
||||
}
|
||||
}
|
||||
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
path = file("src/main/cpp/CMakeLists.txt")
|
||||
version = "3.22.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(17)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
// AndroidX Core
|
||||
implementation(libs.androidx.ktx)
|
||||
implementation(libs.runtime.ktx)
|
||||
@@ -41,6 +137,10 @@ dependencies {
|
||||
// Placeholder
|
||||
implementation(libs.placeholder.material3)
|
||||
|
||||
// HTML Scraper
|
||||
implementation(libs.skrapeit.dsl)
|
||||
implementation(libs.skrapeit.parser)
|
||||
|
||||
// Coil (async image loading, network image)
|
||||
implementation(libs.coil.compose)
|
||||
implementation(libs.coil.appiconloader)
|
||||
@@ -61,7 +161,7 @@ dependencies {
|
||||
implementation(libs.revanced.library)
|
||||
|
||||
// Downloader plugins
|
||||
implementation(project(":api"))
|
||||
implementation(project(":downloader-plugin"))
|
||||
|
||||
// Native processes
|
||||
implementation(libs.kotlin.process)
|
||||
@@ -108,167 +208,4 @@ dependencies {
|
||||
|
||||
// Compose Icons
|
||||
implementation(libs.compose.icons.fontawesome)
|
||||
|
||||
// Ackpine
|
||||
implementation(libs.ackpine.core)
|
||||
implementation(libs.ackpine.ktx)
|
||||
}
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
// Semantic versioning string parser
|
||||
classpath(libs.semver.parser)
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "app.revanced.manager"
|
||||
compileSdk = 35
|
||||
buildToolsVersion = "35.0.1"
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "app.revanced.manager"
|
||||
minSdk = 26
|
||||
targetSdk = 35
|
||||
|
||||
val versionStr = if (version == "unspecified") "1.0.0" else version.toString()
|
||||
versionName = versionStr
|
||||
versionCode = with(versionStr.toVersion()) {
|
||||
major * 10_000_000 +
|
||||
minor * 10_000 +
|
||||
patch * 100 +
|
||||
(preRelease?.substringAfterLast('.')?.toInt() ?: 99)
|
||||
}
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
applicationIdSuffix = ".debug"
|
||||
resValue("string", "app_name", "ReVanced Manager (Debug)")
|
||||
isPseudoLocalesEnabled = true
|
||||
|
||||
buildConfigField("long", "BUILD_ID", "${Random.nextLong()}L")
|
||||
}
|
||||
|
||||
release {
|
||||
if (!project.hasProperty("noProguard")) {
|
||||
isMinifyEnabled = true
|
||||
isShrinkResources = true
|
||||
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
||||
}
|
||||
|
||||
val keystoreFile = file("keystore.jks")
|
||||
|
||||
if (project.hasProperty("signAsDebug") || !keystoreFile.exists()) {
|
||||
applicationIdSuffix = ".debug_signed"
|
||||
resValue("string", "app_name", "ReVanced Manager (Debug signed)")
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
|
||||
isPseudoLocalesEnabled = true
|
||||
} else {
|
||||
signingConfig = signingConfigs.create("release") {
|
||||
storeFile = keystoreFile
|
||||
storePassword = System.getenv("KEYSTORE_PASSWORD")
|
||||
keyAlias = System.getenv("KEYSTORE_ENTRY_ALIAS")
|
||||
keyPassword = System.getenv("KEYSTORE_ENTRY_PASSWORD")
|
||||
}
|
||||
}
|
||||
|
||||
buildConfigField("long", "BUILD_ID", "0L")
|
||||
}
|
||||
}
|
||||
|
||||
applicationVariants.all {
|
||||
outputs.all {
|
||||
this as com.android.build.gradle.internal.api.ApkVariantOutputImpl
|
||||
|
||||
outputFileName = outputApkFileName
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
dependenciesInfo {
|
||||
includeInApk = false
|
||||
includeInBundle = false
|
||||
}
|
||||
|
||||
packaging {
|
||||
resources.excludes.addAll(
|
||||
listOf(
|
||||
"/prebuilt/**",
|
||||
"META-INF/DEPENDENCIES",
|
||||
"META-INF/**.version",
|
||||
"DebugProbesKt.bin",
|
||||
"kotlin-tooling-metadata.json",
|
||||
"org/bouncycastle/pqc/**.properties",
|
||||
"org/bouncycastle/x509/**.properties",
|
||||
)
|
||||
)
|
||||
jniLibs {
|
||||
useLegacyPackaging = true
|
||||
}
|
||||
}
|
||||
|
||||
ksp {
|
||||
arg("room.schemaLocation", "$projectDir/schemas")
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "17"
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
compose = true
|
||||
aidl = true
|
||||
buildConfig = true
|
||||
}
|
||||
|
||||
android {
|
||||
androidResources {
|
||||
generateLocaleConfig = true
|
||||
}
|
||||
}
|
||||
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
path = file("src/main/cpp/CMakeLists.txt")
|
||||
version = "3.22.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(17)
|
||||
}
|
||||
|
||||
tasks {
|
||||
// Needed by gradle-semantic-release-plugin.
|
||||
// Tracking: https://github.com/KengoTODA/gradle-semantic-release-plugin/issues/435.
|
||||
val publish by registering {
|
||||
group = "publishing"
|
||||
description = "Build the release APK"
|
||||
|
||||
dependsOn("assembleRelease")
|
||||
|
||||
val apk = project.layout.buildDirectory.file("outputs/apk/release/${outputApkFileName}")
|
||||
val ascFile = apk.map { it.asFile.resolveSibling("${it.asFile.name}.asc") }
|
||||
|
||||
inputs.file(apk).withPropertyName("inputApk")
|
||||
outputs.file(ascFile).withPropertyName("outputAsc")
|
||||
|
||||
doLast {
|
||||
signing {
|
||||
useGpgCmd()
|
||||
sign(apk.get().asFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
version = 1.26.0-dev.16
|
||||
1
app/gradlew
vendored
1
app/gradlew
vendored
@@ -1 +0,0 @@
|
||||
../gradlew
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"name": "app",
|
||||
"private": false,
|
||||
"devDependencies": {
|
||||
"@anolilab/multi-semantic-release": "^1.1.10",
|
||||
"@saithodev/semantic-release-backmerge": "^4.0.1",
|
||||
"@semantic-release/changelog": "^6.0.3",
|
||||
"@semantic-release/git": "^10.0.1",
|
||||
"gradle-semantic-release-plugin": "^1.10.1"
|
||||
}
|
||||
}
|
||||
67
app/proguard-rules.pro
vendored
67
app/proguard-rules.pro
vendored
@@ -1,14 +1,63 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.kts.kts.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
-dontobfuscate
|
||||
|
||||
-keep class app.revanced.manager.patcher.runtime.process.* { *; }
|
||||
-keep class app.revanced.manager.plugin.** { *; }
|
||||
-keep class app.revanced.patcher.** { *; }
|
||||
-keep class com.android.tools.smali.** { *; }
|
||||
-keep class kotlin.** { *; }
|
||||
-keepnames class com.android.apksig.internal.** { *; }
|
||||
-keepnames class org.xmlpull.** { *; }
|
||||
# Required for serialization to work properly
|
||||
-if @kotlinx.serialization.Serializable class **
|
||||
-keepclassmembers class <1> {
|
||||
static <1>$Companion Companion;
|
||||
}
|
||||
-if @kotlinx.serialization.Serializable class ** {
|
||||
static **$* *;
|
||||
}
|
||||
-keepclassmembers class <2>$<3> {
|
||||
kotlinx.serialization.KSerializer serializer(...);
|
||||
}
|
||||
-if @kotlinx.serialization.Serializable class ** {
|
||||
public static ** INSTANCE;
|
||||
}
|
||||
-keepclassmembers class <1> {
|
||||
public static <1> INSTANCE;
|
||||
kotlinx.serialization.KSerializer serializer(...);
|
||||
}
|
||||
|
||||
-dontwarn com.google.j2objc.annotations.*
|
||||
# This required for the process runtime.
|
||||
-keep class app.revanced.manager.patcher.runtime.process.* {
|
||||
*;
|
||||
}
|
||||
# Required for the patcher to function correctly
|
||||
-keep class app.revanced.patcher.** {
|
||||
*;
|
||||
}
|
||||
-keep class brut.** {
|
||||
*;
|
||||
}
|
||||
-keep class org.xmlpull.** {
|
||||
*;
|
||||
}
|
||||
-keep class kotlin.** {
|
||||
*;
|
||||
}
|
||||
-keep class org.jf.** {
|
||||
*;
|
||||
}
|
||||
-keep class com.android.** {
|
||||
*;
|
||||
}
|
||||
-keep class app.revanced.manager.plugin.** {
|
||||
*;
|
||||
}
|
||||
|
||||
-dontwarn com.google.auto.value.**
|
||||
-dontwarn java.awt.**
|
||||
-dontwarn javax.**
|
||||
-dontwarn org.slf4j.**
|
||||
-dontwarn org.slf4j.**
|
||||
-dontwarn it.skrape.fetcher.*
|
||||
-dontwarn com.google.j2objc.annotations.*
|
||||
|
||||
-keepattributes RuntimeVisibleAnnotations,AnnotationDefault
|
||||
@@ -23,7 +23,6 @@
|
||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
|
||||
tools:ignore="ScopedStorage" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.ENFORCE_UPDATE_OWNERSHIP" />
|
||||
|
||||
<application
|
||||
android:name=".ManagerApplication"
|
||||
@@ -51,6 +50,9 @@
|
||||
|
||||
<activity android:name=".plugin.downloader.webview.WebViewActivity" android:exported="false" android:theme="@style/Theme.WebViewActivity" />
|
||||
|
||||
<service android:name=".service.InstallService" />
|
||||
<service android:name=".service.UninstallService" />
|
||||
|
||||
<service
|
||||
android:name="androidx.work.impl.foreground.SystemForegroundService"
|
||||
android:foregroundServiceType="specialUse"
|
||||
@@ -72,15 +74,5 @@
|
||||
android:value="androidx.startup"
|
||||
tools:node="remove" />
|
||||
</provider>
|
||||
|
||||
<provider
|
||||
android:name="androidx.startup.InitializationProvider"
|
||||
android:authorities="${applicationId}.androidx-startup"
|
||||
android:exported="false"
|
||||
tools:node="merge">
|
||||
<meta-data
|
||||
android:name="ru.solrudev.ackpine.AckpineInitializer"
|
||||
tools:node="remove" />
|
||||
</provider>
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -1,4 +0,0 @@
|
||||
// ProgressEventParcel.aidl
|
||||
package app.revanced.manager.patcher;
|
||||
|
||||
parcelable ProgressEventParcel;
|
||||
@@ -1,12 +1,11 @@
|
||||
// IPatcherEvents.aidl
|
||||
package app.revanced.manager.patcher.runtime.process;
|
||||
|
||||
import app.revanced.manager.patcher.ProgressEventParcel;
|
||||
|
||||
// Interface for sending events back to the main app process.
|
||||
oneway interface IPatcherEvents {
|
||||
void log(String level, String msg);
|
||||
void event(in ProgressEventParcel event);
|
||||
void patchSucceeded();
|
||||
void progress(String name, String state, String msg);
|
||||
// The patching process has ended. The exceptionStackTrace is null if it finished successfully.
|
||||
void finished(String exceptionStackTrace);
|
||||
}
|
||||
@@ -30,7 +30,7 @@ import app.revanced.manager.ui.model.navigation.ComplexParameter
|
||||
import app.revanced.manager.ui.model.navigation.Dashboard
|
||||
import app.revanced.manager.ui.model.navigation.InstalledApplicationInfo
|
||||
import app.revanced.manager.ui.model.navigation.Patcher
|
||||
import app.revanced.manager.ui.model.navigation.SelectedAppInfo
|
||||
import app.revanced.manager.ui.model.navigation.SelectedApplicationInfo
|
||||
import app.revanced.manager.ui.model.navigation.Settings
|
||||
import app.revanced.manager.ui.model.navigation.Update
|
||||
import app.revanced.manager.ui.screen.AppSelectorScreen
|
||||
@@ -41,19 +41,16 @@ import app.revanced.manager.ui.screen.PatchesSelectorScreen
|
||||
import app.revanced.manager.ui.screen.RequiredOptionsScreen
|
||||
import app.revanced.manager.ui.screen.SelectedAppInfoScreen
|
||||
import app.revanced.manager.ui.screen.SettingsScreen
|
||||
import app.revanced.manager.ui.screen.SourceSelectorScreen
|
||||
import app.revanced.manager.ui.screen.UpdateScreen
|
||||
import app.revanced.manager.ui.screen.VersionSelectorScreen
|
||||
import app.revanced.manager.ui.screen.settings.AboutSettingsScreen
|
||||
import app.revanced.manager.ui.screen.settings.AdvancedSettingsScreen
|
||||
import app.revanced.manager.ui.screen.settings.ContributorSettingsScreen
|
||||
import app.revanced.manager.ui.screen.settings.DeveloperSettingsScreen
|
||||
import app.revanced.manager.ui.screen.settings.BackupRestoreSettingsScreen
|
||||
import app.revanced.manager.ui.screen.settings.ContributorScreen
|
||||
import app.revanced.manager.ui.screen.settings.DeveloperOptionsScreen
|
||||
import app.revanced.manager.ui.screen.settings.DownloadsSettingsScreen
|
||||
import app.revanced.manager.ui.screen.settings.GeneralSettingsScreen
|
||||
import app.revanced.manager.ui.screen.settings.ImportExportSettingsScreen
|
||||
import app.revanced.manager.ui.screen.settings.LicensesSettingsScreen
|
||||
import app.revanced.manager.ui.screen.settings.update.ChangelogsSettingsScreen
|
||||
import app.revanced.manager.ui.screen.settings.update.UpdatesSettingsScreen
|
||||
import app.revanced.manager.ui.screen.settings.LicensesScreen
|
||||
import app.revanced.manager.ui.screen.settings.update.ChangelogsScreen
|
||||
import app.revanced.manager.ui.theme.ReVancedManagerTheme
|
||||
import app.revanced.manager.ui.theme.Theme
|
||||
import app.revanced.manager.ui.viewmodel.MainViewModel
|
||||
@@ -83,7 +80,6 @@ class MainActivity : ComponentActivity() {
|
||||
)
|
||||
val theme by vm.prefs.theme.getAsState()
|
||||
val dynamicColor by vm.prefs.dynamicColor.getAsState()
|
||||
val pureBlackTheme by vm.prefs.pureBlackTheme.getAsState()
|
||||
|
||||
EventEffect(vm.legacyImportActivityFlow) {
|
||||
try {
|
||||
@@ -94,19 +90,25 @@ class MainActivity : ComponentActivity() {
|
||||
|
||||
ReVancedManagerTheme(
|
||||
darkTheme = theme == Theme.SYSTEM && isSystemInDarkTheme() || theme == Theme.DARK,
|
||||
dynamicColor = dynamicColor,
|
||||
pureBlackTheme = pureBlackTheme
|
||||
dynamicColor = dynamicColor
|
||||
) {
|
||||
ReVancedManager()
|
||||
ReVancedManager(vm)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ReVancedManager() {
|
||||
private fun ReVancedManager(vm: MainViewModel) {
|
||||
val navController = rememberNavController()
|
||||
|
||||
EventEffect(vm.appSelectFlow) { app ->
|
||||
navController.navigateComplex(
|
||||
SelectedApplicationInfo,
|
||||
SelectedApplicationInfo.ViewModelParams(app)
|
||||
)
|
||||
}
|
||||
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = Dashboard,
|
||||
@@ -137,12 +139,7 @@ private fun ReVancedManager() {
|
||||
val data = it.toRoute<InstalledApplicationInfo>()
|
||||
|
||||
InstalledAppInfoScreen(
|
||||
onPatchClick = { packageName ->
|
||||
navController.navigateComplex(
|
||||
SelectedAppInfo,
|
||||
SelectedAppInfo.ViewModelParams(packageName)
|
||||
)
|
||||
},
|
||||
onPatchClick = vm::selectApp,
|
||||
onBackClick = navController::popBackStack,
|
||||
viewModel = koinViewModel { parametersOf(data.packageName) }
|
||||
)
|
||||
@@ -150,20 +147,8 @@ private fun ReVancedManager() {
|
||||
|
||||
composable<AppSelector> {
|
||||
AppSelectorScreen(
|
||||
onSelect = { packageName ->
|
||||
navController.navigateComplex(
|
||||
SelectedAppInfo,
|
||||
SelectedAppInfo.ViewModelParams(packageName)
|
||||
)
|
||||
},
|
||||
onStorageSelect = { packageName, localPath ->
|
||||
navController.navigateComplex(
|
||||
SelectedAppInfo,
|
||||
SelectedAppInfo.ViewModelParams(
|
||||
packageName, localPath
|
||||
)
|
||||
)
|
||||
},
|
||||
onSelect = vm::selectApp,
|
||||
onStorageSelect = vm::selectApp,
|
||||
onBackClick = navController::popBackStack
|
||||
)
|
||||
}
|
||||
@@ -178,7 +163,7 @@ private fun ReVancedManager() {
|
||||
}
|
||||
}
|
||||
},
|
||||
viewModel = koinViewModel { parametersOf(it.getComplexArg<Patcher.ViewModelParams>()) }
|
||||
vm = koinViewModel { parametersOf(it.getComplexArg<Patcher.ViewModelParams>()) }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -191,11 +176,11 @@ private fun ReVancedManager() {
|
||||
)
|
||||
}
|
||||
|
||||
navigation<SelectedAppInfo>(startDestination = SelectedAppInfo.Main) {
|
||||
composable<SelectedAppInfo.Main> {
|
||||
navigation<SelectedApplicationInfo>(startDestination = SelectedApplicationInfo.Main) {
|
||||
composable<SelectedApplicationInfo.Main> {
|
||||
val parentBackStackEntry = navController.navGraphEntry(it)
|
||||
val data =
|
||||
parentBackStackEntry.getComplexArg<SelectedAppInfo.ViewModelParams>()
|
||||
parentBackStackEntry.getComplexArg<SelectedApplicationInfo.ViewModelParams>()
|
||||
val viewModel =
|
||||
koinNavViewModel<SelectedAppInfoViewModel>(viewModelStoreOwner = parentBackStackEntry) {
|
||||
parametersOf(data)
|
||||
@@ -211,47 +196,23 @@ private fun ReVancedManager() {
|
||||
)
|
||||
}
|
||||
},
|
||||
onPatchSelectorClick = { packageName, version, patchSelection, options ->
|
||||
onPatchSelectorClick = { app, patches, options ->
|
||||
navController.navigateComplex(
|
||||
SelectedAppInfo.PatchesSelector,
|
||||
SelectedAppInfo.PatchesSelector.ViewModelParams(
|
||||
packageName,
|
||||
version,
|
||||
patchSelection,
|
||||
options,
|
||||
SelectedApplicationInfo.PatchesSelector,
|
||||
SelectedApplicationInfo.PatchesSelector.ViewModelParams(
|
||||
app,
|
||||
patches,
|
||||
options
|
||||
)
|
||||
)
|
||||
},
|
||||
onRequiredOptions = { packageName, version, patchSelection, options ->
|
||||
onRequiredOptions = { app, patches, options ->
|
||||
navController.navigateComplex(
|
||||
SelectedAppInfo.RequiredOptions,
|
||||
SelectedAppInfo.PatchesSelector.ViewModelParams(
|
||||
packageName,
|
||||
version,
|
||||
patchSelection,
|
||||
options,
|
||||
)
|
||||
)
|
||||
},
|
||||
onVersionClick = { packageName, patchSelection, selectedVersion, local ->
|
||||
navController.navigateComplex(
|
||||
SelectedAppInfo.VersionSelector,
|
||||
SelectedAppInfo.VersionSelector.ViewModelParams(
|
||||
packageName,
|
||||
patchSelection,
|
||||
selectedVersion,
|
||||
local,
|
||||
)
|
||||
)
|
||||
},
|
||||
onSourceClick = { packageName, version, selectedSource, local ->
|
||||
navController.navigateComplex(
|
||||
SelectedAppInfo.SourceSelector,
|
||||
SelectedAppInfo.SourceSelector.ViewModelParams(
|
||||
packageName,
|
||||
version,
|
||||
selectedSource,
|
||||
local,
|
||||
SelectedApplicationInfo.RequiredOptions,
|
||||
SelectedApplicationInfo.PatchesSelector.ViewModelParams(
|
||||
app,
|
||||
patches,
|
||||
options
|
||||
)
|
||||
)
|
||||
},
|
||||
@@ -259,9 +220,9 @@ private fun ReVancedManager() {
|
||||
)
|
||||
}
|
||||
|
||||
composable<SelectedAppInfo.PatchesSelector> {
|
||||
composable<SelectedApplicationInfo.PatchesSelector> {
|
||||
val data =
|
||||
it.getComplexArg<SelectedAppInfo.PatchesSelector.ViewModelParams>()
|
||||
it.getComplexArg<SelectedApplicationInfo.PatchesSelector.ViewModelParams>()
|
||||
val selectedAppInfoVm = koinNavViewModel<SelectedAppInfoViewModel>(
|
||||
viewModelStoreOwner = navController.navGraphEntry(it)
|
||||
)
|
||||
@@ -272,47 +233,13 @@ private fun ReVancedManager() {
|
||||
selectedAppInfoVm.updateConfiguration(patches, options)
|
||||
navController.popBackStack()
|
||||
},
|
||||
viewModel = koinViewModel { parametersOf(data) }
|
||||
vm = koinViewModel { parametersOf(data) }
|
||||
)
|
||||
}
|
||||
|
||||
composable<SelectedAppInfo.VersionSelector> {
|
||||
composable<SelectedApplicationInfo.RequiredOptions> {
|
||||
val data =
|
||||
it.getComplexArg<SelectedAppInfo.VersionSelector.ViewModelParams>()
|
||||
val selectedAppInfoVm = koinNavViewModel<SelectedAppInfoViewModel>(
|
||||
viewModelStoreOwner = navController.navGraphEntry(it)
|
||||
)
|
||||
|
||||
VersionSelectorScreen(
|
||||
onBackClick = navController::popBackStack,
|
||||
onSave = { version ->
|
||||
selectedAppInfoVm.updateVersion(version)
|
||||
navController.popBackStack()
|
||||
},
|
||||
viewModel = koinViewModel { parametersOf(data) }
|
||||
)
|
||||
}
|
||||
|
||||
composable<SelectedAppInfo.SourceSelector> {
|
||||
val data =
|
||||
it.getComplexArg<SelectedAppInfo.SourceSelector.ViewModelParams>()
|
||||
val selectedAppInfoVm = koinNavViewModel<SelectedAppInfoViewModel>(
|
||||
viewModelStoreOwner = navController.navGraphEntry(it)
|
||||
)
|
||||
|
||||
SourceSelectorScreen(
|
||||
onBackClick = navController::popBackStack,
|
||||
onSave = { source ->
|
||||
selectedAppInfoVm.updateSource(source)
|
||||
navController.popBackStack()
|
||||
},
|
||||
viewModel = koinViewModel { parametersOf(data) }
|
||||
)
|
||||
}
|
||||
|
||||
composable<SelectedAppInfo.RequiredOptions> {
|
||||
val data =
|
||||
it.getComplexArg<SelectedAppInfo.PatchesSelector.ViewModelParams>()
|
||||
it.getComplexArg<SelectedApplicationInfo.PatchesSelector.ViewModelParams>()
|
||||
val selectedAppInfoVm = koinNavViewModel<SelectedAppInfoViewModel>(
|
||||
viewModelStoreOwner = navController.navGraphEntry(it)
|
||||
)
|
||||
@@ -342,52 +269,47 @@ private fun ReVancedManager() {
|
||||
}
|
||||
|
||||
composable<Settings.General> {
|
||||
GeneralSettingsScreen(onBackClick = navController::popBackStack)
|
||||
GeneralSettingsScreen(
|
||||
onBackClick = navController::popBackStack,
|
||||
onUpdateClick = { navController.navigate(Update()) }
|
||||
)
|
||||
}
|
||||
|
||||
composable<Settings.Advanced> {
|
||||
AdvancedSettingsScreen(onBackClick = navController::popBackStack)
|
||||
}
|
||||
|
||||
composable<Settings.Developer> {
|
||||
DeveloperSettingsScreen(onBackClick = navController::popBackStack)
|
||||
}
|
||||
|
||||
composable<Settings.Updates> {
|
||||
UpdatesSettingsScreen(
|
||||
onBackClick = navController::popBackStack,
|
||||
onChangelogClick = { navController.navigate(Settings.Changelogs) },
|
||||
onUpdateClick = { navController.navigate(Update()) }
|
||||
)
|
||||
}
|
||||
|
||||
composable<Settings.Downloads> {
|
||||
DownloadsSettingsScreen(onBackClick = navController::popBackStack)
|
||||
}
|
||||
|
||||
composable<Settings.ImportExport> {
|
||||
ImportExportSettingsScreen(onBackClick = navController::popBackStack)
|
||||
BackupRestoreSettingsScreen(onBackClick = navController::popBackStack)
|
||||
}
|
||||
|
||||
composable<Settings.About> {
|
||||
AboutSettingsScreen(
|
||||
onBackClick = navController::popBackStack,
|
||||
onChangelogClick = { navController.navigate(Settings.Changelogs) },
|
||||
navigate = navController::navigate
|
||||
)
|
||||
}
|
||||
|
||||
composable<Settings.Changelogs> {
|
||||
ChangelogsSettingsScreen(onBackClick = navController::popBackStack)
|
||||
ChangelogsScreen(onBackClick = navController::popBackStack)
|
||||
}
|
||||
|
||||
composable<Settings.Contributors> {
|
||||
ContributorSettingsScreen(onBackClick = navController::popBackStack)
|
||||
ContributorScreen(onBackClick = navController::popBackStack)
|
||||
}
|
||||
|
||||
composable<Settings.Licenses> {
|
||||
LicensesSettingsScreen(onBackClick = navController::popBackStack)
|
||||
LicensesScreen(onBackClick = navController::popBackStack)
|
||||
}
|
||||
|
||||
composable<Settings.DeveloperOptions> {
|
||||
DeveloperOptionsScreen(onBackClick = navController::popBackStack)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,8 +48,7 @@ class ManagerApplication : Application() {
|
||||
workerModule,
|
||||
viewModelModule,
|
||||
databaseModule,
|
||||
rootModule,
|
||||
ackpineModule
|
||||
rootModule
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
package app.revanced.manager.data.redux
|
||||
|
||||
import android.util.Log
|
||||
import app.revanced.manager.util.tag
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
|
||||
// This file implements React Redux-like state management.
|
||||
|
||||
class Store<S>(private val coroutineScope: CoroutineScope, initialState: S) : ActionContext {
|
||||
private val _state = MutableStateFlow(initialState)
|
||||
val state = _state.asStateFlow()
|
||||
|
||||
// Do not touch these without the lock.
|
||||
private var isRunningActions = false
|
||||
private val queueChannel = Channel<Action<S>>(capacity = 10)
|
||||
private val lock = Mutex()
|
||||
|
||||
suspend fun dispatch(action: Action<S>) = lock.withLock {
|
||||
Log.d(tag, "Dispatching $action")
|
||||
queueChannel.send(action)
|
||||
|
||||
if (isRunningActions) return@withLock
|
||||
isRunningActions = true
|
||||
coroutineScope.launch {
|
||||
runActions()
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private suspend fun runActions() {
|
||||
while (true) {
|
||||
val action = withTimeoutOrNull(200L) { queueChannel.receive() }
|
||||
if (action == null) {
|
||||
Log.d(tag, "Stopping action runner")
|
||||
lock.withLock {
|
||||
// New actions may be dispatched during the timeout.
|
||||
isRunningActions = !queueChannel.isEmpty
|
||||
if (!isRunningActions) return
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
Log.d(tag, "Running $action")
|
||||
_state.value = try {
|
||||
with(action) { this@Store.execute(_state.value) }
|
||||
} catch (c: CancellationException) {
|
||||
// This is done without the lock, but cancellation usually means the store is no longer needed.
|
||||
isRunningActions = false
|
||||
throw c
|
||||
} catch (e: Exception) {
|
||||
action.catch(e)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface ActionContext
|
||||
|
||||
interface Action<S> {
|
||||
suspend fun ActionContext.execute(current: S): S
|
||||
suspend fun catch(exception: Exception) {
|
||||
Log.e(tag, "Got exception while executing $this", exception)
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package app.revanced.manager.data.room.apps.downloaded
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.Query
|
||||
import androidx.room.Upsert
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
@@ -11,9 +12,6 @@ interface DownloadedAppDao {
|
||||
@Query("SELECT * FROM downloaded_app")
|
||||
fun getAllApps(): Flow<List<DownloadedApp>>
|
||||
|
||||
@Query("SELECT * FROM downloaded_app WHERE package_name = :packageName")
|
||||
fun get(packageName: String): Flow<List<DownloadedApp>>
|
||||
|
||||
@Query("SELECT * FROM downloaded_app WHERE package_name = :packageName AND version = :version")
|
||||
suspend fun get(packageName: String, version: String): DownloadedApp?
|
||||
|
||||
|
||||
@@ -1,14 +1,24 @@
|
||||
package app.revanced.manager.data.room.bundles
|
||||
|
||||
import androidx.room.*
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface PatchBundleDao {
|
||||
@Query("SELECT * FROM patch_bundles")
|
||||
suspend fun all(): List<PatchBundleEntity>
|
||||
|
||||
@Query("SELECT version, auto_update FROM patch_bundles WHERE uid = :uid")
|
||||
fun getPropsById(uid: Int): Flow<BundleProperties?>
|
||||
|
||||
@Query("UPDATE patch_bundles SET version = :patches WHERE uid = :uid")
|
||||
suspend fun updateVersionHash(uid: Int, patches: String?)
|
||||
suspend fun updateVersion(uid: Int, patches: String?)
|
||||
|
||||
@Query("UPDATE patch_bundles SET auto_update = :value WHERE uid = :uid")
|
||||
suspend fun setAutoUpdate(uid: Int, value: Boolean)
|
||||
|
||||
@Query("UPDATE patch_bundles SET name = :value WHERE uid = :uid")
|
||||
suspend fun setName(uid: Int, value: String)
|
||||
|
||||
@Query("DELETE FROM patch_bundles WHERE uid != 0")
|
||||
suspend fun purgeCustomBundles()
|
||||
@@ -16,15 +26,12 @@ interface PatchBundleDao {
|
||||
@Transaction
|
||||
suspend fun reset() {
|
||||
purgeCustomBundles()
|
||||
updateVersionHash(0, null) // Reset the main source
|
||||
updateVersion(0, null) // Reset the main source
|
||||
}
|
||||
|
||||
@Query("DELETE FROM patch_bundles WHERE uid = :uid")
|
||||
suspend fun remove(uid: Int)
|
||||
|
||||
@Query("SELECT name, version, auto_update, source FROM patch_bundles WHERE uid = :uid")
|
||||
suspend fun getProps(uid: Int): PatchBundleProperties?
|
||||
|
||||
@Upsert
|
||||
suspend fun upsert(source: PatchBundleEntity)
|
||||
@Insert
|
||||
suspend fun add(source: PatchBundleEntity)
|
||||
}
|
||||
@@ -33,14 +33,12 @@ sealed class Source {
|
||||
data class PatchBundleEntity(
|
||||
@PrimaryKey val uid: Int,
|
||||
@ColumnInfo(name = "name") val name: String,
|
||||
@ColumnInfo(name = "version") val versionHash: String? = null,
|
||||
@ColumnInfo(name = "version") val version: String? = null,
|
||||
@ColumnInfo(name = "source") val source: Source,
|
||||
@ColumnInfo(name = "auto_update") val autoUpdate: Boolean
|
||||
)
|
||||
|
||||
data class PatchBundleProperties(
|
||||
@ColumnInfo(name = "name") val name: String,
|
||||
@ColumnInfo(name = "version") val versionHash: String? = null,
|
||||
@ColumnInfo(name = "source") val source: Source,
|
||||
data class BundleProperties(
|
||||
@ColumnInfo(name = "version") val version: String? = null,
|
||||
@ColumnInfo(name = "auto_update") val autoUpdate: Boolean
|
||||
)
|
||||
@@ -27,10 +27,10 @@ abstract class OptionDao {
|
||||
abstract suspend fun createOptionGroup(group: OptionGroup)
|
||||
|
||||
@Query("DELETE FROM option_groups WHERE patch_bundle = :uid")
|
||||
abstract suspend fun resetOptionsForPatchBundle(uid: Int)
|
||||
abstract suspend fun clearForPatchBundle(uid: Int)
|
||||
|
||||
@Query("DELETE FROM option_groups WHERE package_name = :packageName")
|
||||
abstract suspend fun resetOptionsForPackage(packageName: String)
|
||||
abstract suspend fun clearForPackage(packageName: String)
|
||||
|
||||
@Query("DELETE FROM option_groups")
|
||||
abstract suspend fun reset()
|
||||
|
||||
@@ -5,7 +5,6 @@ import androidx.room.Insert
|
||||
import androidx.room.MapColumn
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
abstract class SelectionDao {
|
||||
@@ -35,14 +34,11 @@ abstract class SelectionDao {
|
||||
@Insert
|
||||
abstract suspend fun createSelection(selection: PatchSelection)
|
||||
|
||||
@Query("SELECT package_name FROM patch_selections")
|
||||
abstract fun getPackagesWithSelection(): Flow<List<String>>
|
||||
|
||||
@Query("DELETE FROM patch_selections WHERE patch_bundle = :uid")
|
||||
abstract suspend fun resetForPatchBundle(uid: Int)
|
||||
abstract suspend fun clearForPatchBundle(uid: Int)
|
||||
|
||||
@Query("DELETE FROM patch_selections WHERE package_name = :packageName")
|
||||
abstract suspend fun resetForPackage(packageName: String)
|
||||
abstract suspend fun clearForPackage(packageName: String)
|
||||
|
||||
@Query("DELETE FROM patch_selections")
|
||||
abstract suspend fun reset()
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
package app.revanced.manager.di
|
||||
|
||||
import android.content.Context
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.dsl.module
|
||||
import ru.solrudev.ackpine.installer.PackageInstaller
|
||||
import ru.solrudev.ackpine.uninstaller.PackageUninstaller
|
||||
|
||||
val ackpineModule = module {
|
||||
fun provideInstaller(context: Context) = PackageInstaller.getInstance(context)
|
||||
fun provideUninstaller(context: Context) = PackageUninstaller.getInstance(context)
|
||||
|
||||
single {
|
||||
provideInstaller(androidContext())
|
||||
}
|
||||
single {
|
||||
provideUninstaller(androidContext())
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ val repositoryModule = module {
|
||||
createdAtStart()
|
||||
}
|
||||
singleOf(::NetworkInfo)
|
||||
singleOf(::PatchBundlePersistenceRepository)
|
||||
singleOf(::PatchSelectionRepository)
|
||||
singleOf(::PatchOptionsRepository)
|
||||
singleOf(::PatchBundleRepository) {
|
||||
|
||||
@@ -23,7 +23,4 @@ val viewModelModule = module {
|
||||
viewModelOf(::InstalledAppsViewModel)
|
||||
viewModelOf(::InstalledAppInfoViewModel)
|
||||
viewModelOf(::UpdatesSettingsViewModel)
|
||||
viewModelOf(::BundleListViewModel)
|
||||
viewModelOf(::VersionSelectorViewModel)
|
||||
viewModelOf(::SourceSelectorViewModel)
|
||||
}
|
||||
|
||||
@@ -1,29 +1,21 @@
|
||||
package app.revanced.manager.domain.bundles
|
||||
|
||||
import app.revanced.manager.data.redux.ActionContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
|
||||
class LocalPatchBundle(
|
||||
name: String,
|
||||
uid: Int,
|
||||
error: Throwable?,
|
||||
directory: File
|
||||
) : PatchBundleSource(name, uid, error, directory) {
|
||||
suspend fun ActionContext.replace(patches: InputStream) {
|
||||
class LocalPatchBundle(name: String, id: Int, directory: File) :
|
||||
PatchBundleSource(name, id, directory) {
|
||||
suspend fun replace(patches: InputStream) {
|
||||
withContext(Dispatchers.IO) {
|
||||
patchBundleOutputStream().use { outputStream ->
|
||||
patches.copyTo(outputStream)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun copy(error: Throwable?, name: String) = LocalPatchBundle(
|
||||
name,
|
||||
uid,
|
||||
error,
|
||||
directory
|
||||
)
|
||||
reload()?.also {
|
||||
saveVersion(it.readManifestAttribute("Version"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,22 @@
|
||||
package app.revanced.manager.domain.bundles
|
||||
|
||||
import android.app.Application
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import app.revanced.manager.data.redux.ActionContext
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.domain.repository.PatchBundlePersistenceRepository
|
||||
import app.revanced.manager.patcher.patch.PatchBundle
|
||||
import app.revanced.manager.util.tag
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import java.io.File
|
||||
import java.io.OutputStream
|
||||
|
||||
@@ -12,32 +24,24 @@ import java.io.OutputStream
|
||||
* A [PatchBundle] source.
|
||||
*/
|
||||
@Stable
|
||||
sealed class PatchBundleSource(
|
||||
val name: String,
|
||||
val uid: Int,
|
||||
error: Throwable?,
|
||||
protected val directory: File
|
||||
) {
|
||||
sealed class PatchBundleSource(initialName: String, val uid: Int, directory: File) : KoinComponent {
|
||||
protected val configRepository: PatchBundlePersistenceRepository by inject()
|
||||
private val app: Application by inject()
|
||||
protected val patchesFile = directory.resolve("patches.jar")
|
||||
|
||||
val state = when {
|
||||
error != null -> State.Failed(error)
|
||||
!hasInstalled() -> State.Missing
|
||||
else -> State.Available(PatchBundle(patchesFile.absolutePath))
|
||||
}
|
||||
private val _state = MutableStateFlow(load())
|
||||
val state = _state.asStateFlow()
|
||||
|
||||
val patchBundle get() = (state as? State.Available)?.bundle
|
||||
val version get() = patchBundle?.manifestAttributes?.version
|
||||
val isNameOutOfDate get() = patchBundle?.manifestAttributes?.name?.let { it != name } == true
|
||||
val error get() = (state as? State.Failed)?.throwable
|
||||
private val _nameFlow = MutableStateFlow(initialName)
|
||||
val nameFlow =
|
||||
_nameFlow.map { it.ifEmpty { app.getString(if (isDefault) R.string.bundle_name_default else R.string.bundle_name_fallback) } }
|
||||
|
||||
suspend fun ActionContext.deleteLocalFile() = withContext(Dispatchers.IO) {
|
||||
patchesFile.delete()
|
||||
}
|
||||
suspend fun getName() = nameFlow.first()
|
||||
|
||||
abstract fun copy(error: Throwable? = this.error, name: String = this.name): PatchBundleSource
|
||||
|
||||
protected fun hasInstalled() = patchesFile.exists()
|
||||
/**
|
||||
* Returns true if the bundle has been downloaded to local storage.
|
||||
*/
|
||||
fun hasInstalled() = patchesFile.exists()
|
||||
|
||||
protected fun patchBundleOutputStream(): OutputStream = with(patchesFile) {
|
||||
// Android 14+ requires dex containers to be readonly.
|
||||
@@ -49,14 +53,62 @@ sealed class PatchBundleSource(
|
||||
}
|
||||
}
|
||||
|
||||
private fun load(): State {
|
||||
if (!hasInstalled()) return State.Missing
|
||||
|
||||
return try {
|
||||
State.Loaded(PatchBundle(patchesFile))
|
||||
} catch (t: Throwable) {
|
||||
Log.e(tag, "Failed to load patch bundle with UID $uid", t)
|
||||
State.Failed(t)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun reload(): PatchBundle? {
|
||||
val newState = load()
|
||||
_state.value = newState
|
||||
|
||||
val bundle = newState.patchBundleOrNull()
|
||||
// Try to read the name from the patch bundle manifest if the bundle does not have a name.
|
||||
if (bundle != null && _nameFlow.value.isEmpty()) {
|
||||
bundle.readManifestAttribute("Name")?.let { setName(it) }
|
||||
}
|
||||
|
||||
return bundle
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a flow that emits the [app.revanced.manager.data.room.bundles.BundleProperties] of this [PatchBundleSource].
|
||||
* The flow will emit null if the associated [PatchBundleSource] is deleted.
|
||||
*/
|
||||
fun propsFlow() = configRepository.getProps(uid).flowOn(Dispatchers.Default)
|
||||
suspend fun getProps() = propsFlow().first()!!
|
||||
|
||||
suspend fun currentVersion() = getProps().version
|
||||
protected suspend fun saveVersion(version: String?) =
|
||||
configRepository.updateVersion(uid, version)
|
||||
|
||||
suspend fun setName(name: String) {
|
||||
configRepository.setName(uid, name)
|
||||
_nameFlow.value = name
|
||||
}
|
||||
|
||||
sealed interface State {
|
||||
fun patchBundleOrNull(): PatchBundle? = null
|
||||
|
||||
data object Missing : State
|
||||
data class Failed(val throwable: Throwable) : State
|
||||
data class Available(val bundle: PatchBundle) : State
|
||||
data class Loaded(val bundle: PatchBundle) : State {
|
||||
override fun patchBundleOrNull() = bundle
|
||||
}
|
||||
}
|
||||
|
||||
companion object Extensions {
|
||||
val PatchBundleSource.isDefault inline get() = uid == 0
|
||||
val PatchBundleSource.asRemoteOrNull inline get() = this as? RemotePatchBundle
|
||||
val PatchBundleSource.nameState
|
||||
@Composable inline get() = nameFlow.collectAsStateWithLifecycle(
|
||||
""
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
package app.revanced.manager.domain.bundles
|
||||
|
||||
import app.revanced.manager.data.redux.ActionContext
|
||||
import androidx.compose.runtime.Stable
|
||||
import app.revanced.manager.network.api.ReVancedAPI
|
||||
import app.revanced.manager.network.dto.ReVancedAsset
|
||||
import app.revanced.manager.network.service.HttpService
|
||||
@@ -8,24 +8,15 @@ import app.revanced.manager.network.utils.getOrThrow
|
||||
import io.ktor.client.request.url
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import java.io.File
|
||||
|
||||
sealed class RemotePatchBundle(
|
||||
name: String,
|
||||
uid: Int,
|
||||
protected val versionHash: String?,
|
||||
error: Throwable?,
|
||||
directory: File,
|
||||
val endpoint: String,
|
||||
val autoUpdate: Boolean,
|
||||
) : PatchBundleSource(name, uid, error, directory), KoinComponent {
|
||||
@Stable
|
||||
sealed class RemotePatchBundle(name: String, id: Int, directory: File, val endpoint: String) :
|
||||
PatchBundleSource(name, id, directory) {
|
||||
protected val http: HttpService by inject()
|
||||
|
||||
protected abstract suspend fun getLatestInfo(): ReVancedAsset
|
||||
abstract fun copy(error: Throwable? = this.error, name: String = this.name, autoUpdate: Boolean = this.autoUpdate): RemotePatchBundle
|
||||
override fun copy(error: Throwable?, name: String): RemotePatchBundle = copy(error, name, this.autoUpdate)
|
||||
|
||||
private suspend fun download(info: ReVancedAsset) = withContext(Dispatchers.IO) {
|
||||
patchBundleOutputStream().use {
|
||||
@@ -34,72 +25,47 @@ sealed class RemotePatchBundle(
|
||||
}
|
||||
}
|
||||
|
||||
info.version
|
||||
saveVersion(info.version)
|
||||
reload()
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads the latest version regardless if there is a new update available.
|
||||
*/
|
||||
suspend fun ActionContext.downloadLatest() = download(getLatestInfo())
|
||||
suspend fun downloadLatest() {
|
||||
download(getLatestInfo())
|
||||
}
|
||||
|
||||
suspend fun ActionContext.update(): String? = withContext(Dispatchers.IO) {
|
||||
suspend fun update(): Boolean = withContext(Dispatchers.IO) {
|
||||
val info = getLatestInfo()
|
||||
if (hasInstalled() && info.version == versionHash)
|
||||
return@withContext null
|
||||
if (hasInstalled() && info.version == currentVersion())
|
||||
return@withContext false
|
||||
|
||||
download(info)
|
||||
true
|
||||
}
|
||||
|
||||
suspend fun deleteLocalFiles() = withContext(Dispatchers.Default) {
|
||||
patchesFile.delete()
|
||||
reload()
|
||||
}
|
||||
|
||||
suspend fun setAutoUpdate(value: Boolean) = configRepository.setAutoUpdate(uid, value)
|
||||
|
||||
companion object {
|
||||
const val updateFailMsg = "Failed to update patches"
|
||||
const val updateFailMsg = "Failed to update patch bundle(s)"
|
||||
}
|
||||
}
|
||||
|
||||
class JsonPatchBundle(
|
||||
name: String,
|
||||
uid: Int,
|
||||
versionHash: String?,
|
||||
error: Throwable?,
|
||||
directory: File,
|
||||
endpoint: String,
|
||||
autoUpdate: Boolean,
|
||||
) : RemotePatchBundle(name, uid, versionHash, error, directory, endpoint, autoUpdate) {
|
||||
class JsonPatchBundle(name: String, id: Int, directory: File, endpoint: String) :
|
||||
RemotePatchBundle(name, id, directory, endpoint) {
|
||||
override suspend fun getLatestInfo() = withContext(Dispatchers.IO) {
|
||||
http.request<ReVancedAsset> {
|
||||
url(endpoint)
|
||||
}.getOrThrow()
|
||||
}
|
||||
|
||||
override fun copy(error: Throwable?, name: String, autoUpdate: Boolean) = JsonPatchBundle(
|
||||
name,
|
||||
uid,
|
||||
versionHash,
|
||||
error,
|
||||
directory,
|
||||
endpoint,
|
||||
autoUpdate,
|
||||
)
|
||||
}
|
||||
|
||||
class APIPatchBundle(
|
||||
name: String,
|
||||
uid: Int,
|
||||
versionHash: String?,
|
||||
error: Throwable?,
|
||||
directory: File,
|
||||
endpoint: String,
|
||||
autoUpdate: Boolean,
|
||||
) : RemotePatchBundle(name, uid, versionHash, error, directory, endpoint, autoUpdate) {
|
||||
class APIPatchBundle(name: String, id: Int, directory: File, endpoint: String) :
|
||||
RemotePatchBundle(name, id, directory, endpoint) {
|
||||
private val api: ReVancedAPI by inject()
|
||||
|
||||
override suspend fun getLatestInfo() = api.getPatchesUpdate().getOrThrow()
|
||||
override fun copy(error: Throwable?, name: String, autoUpdate: Boolean) = APIPatchBundle(
|
||||
name,
|
||||
uid,
|
||||
versionHash,
|
||||
error,
|
||||
directory,
|
||||
endpoint,
|
||||
autoUpdate,
|
||||
)
|
||||
}
|
||||
@@ -27,25 +27,25 @@ class KeystoreManager(app: Application, private val prefs: PreferencesManager) {
|
||||
private val keystorePath =
|
||||
app.getDir("signing", Context.MODE_PRIVATE).resolve("manager.keystore")
|
||||
|
||||
private suspend fun updatePrefs(alias: String, pass: String) = prefs.edit {
|
||||
prefs.keystoreAlias.value = alias
|
||||
private suspend fun updatePrefs(cn: String, pass: String) = prefs.edit {
|
||||
prefs.keystoreCommonName.value = cn
|
||||
prefs.keystorePass.value = pass
|
||||
}
|
||||
|
||||
private suspend fun signingDetails(path: File = keystorePath) = ApkUtils.KeyStoreDetails(
|
||||
keyStore = path,
|
||||
keyStorePassword = null,
|
||||
alias = prefs.keystoreAlias.get(),
|
||||
alias = prefs.keystoreCommonName.get(),
|
||||
password = prefs.keystorePass.get()
|
||||
)
|
||||
|
||||
suspend fun sign(input: File, output: File) = withContext(Dispatchers.Default) {
|
||||
ApkUtils.signApk(input, output, prefs.keystoreAlias.get(), signingDetails())
|
||||
ApkUtils.signApk(input, output, prefs.keystoreCommonName.get(), signingDetails())
|
||||
}
|
||||
|
||||
suspend fun regenerate() = withContext(Dispatchers.Default) {
|
||||
val keyCertPair = ApkSigner.newPrivateKeyCertificatePair(
|
||||
prefs.keystoreAlias.get(),
|
||||
prefs.keystoreCommonName.get(),
|
||||
eightYearsFromNow
|
||||
)
|
||||
val ks = ApkSigner.newKeyStore(
|
||||
@@ -64,13 +64,13 @@ class KeystoreManager(app: Application, private val prefs: PreferencesManager) {
|
||||
updatePrefs(DEFAULT, DEFAULT)
|
||||
}
|
||||
|
||||
suspend fun import(alias: String, pass: String, keystore: InputStream): Boolean {
|
||||
suspend fun import(cn: String, pass: String, keystore: InputStream): Boolean {
|
||||
val keystoreData = withContext(Dispatchers.IO) { keystore.readBytes() }
|
||||
|
||||
try {
|
||||
val ks = ApkSigner.readKeyStore(ByteArrayInputStream(keystoreData), null)
|
||||
|
||||
ApkSigner.readPrivateKeyCertificatePair(ks, alias, pass)
|
||||
ApkSigner.readPrivateKeyCertificatePair(ks, cn, pass)
|
||||
} catch (_: UnrecoverableKeyException) {
|
||||
return false
|
||||
} catch (_: IllegalArgumentException) {
|
||||
@@ -81,7 +81,7 @@ class KeystoreManager(app: Application, private val prefs: PreferencesManager) {
|
||||
Files.write(keystorePath.toPath(), keystoreData)
|
||||
}
|
||||
|
||||
updatePrefs(alias, pass)
|
||||
updatePrefs(cn, pass)
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@@ -3,13 +3,11 @@ package app.revanced.manager.domain.manager
|
||||
import android.content.Context
|
||||
import app.revanced.manager.domain.manager.base.BasePreferencesManager
|
||||
import app.revanced.manager.ui.theme.Theme
|
||||
import app.revanced.manager.util.isDebuggable
|
||||
|
||||
class PreferencesManager(
|
||||
context: Context
|
||||
) : BasePreferencesManager(context, "settings") {
|
||||
val dynamicColor = booleanPreference("dynamic_color", true)
|
||||
val pureBlackTheme = booleanPreference("pure_black_theme", false)
|
||||
val theme = enumPreference("theme", Theme.SYSTEM)
|
||||
|
||||
val api = stringPreference("api_url", "https://api.revanced.app")
|
||||
@@ -17,21 +15,17 @@ class PreferencesManager(
|
||||
val useProcessRuntime = booleanPreference("use_process_runtime", false)
|
||||
val patcherProcessMemoryLimit = intPreference("process_runtime_memory_limit", 700)
|
||||
|
||||
val keystoreAlias = stringPreference("keystore_alias", KeystoreManager.DEFAULT)
|
||||
val keystoreCommonName = stringPreference("keystore_cn", KeystoreManager.DEFAULT)
|
||||
val keystorePass = stringPreference("keystore_pass", KeystoreManager.DEFAULT)
|
||||
|
||||
val firstLaunch = booleanPreference("first_launch", true)
|
||||
val managerAutoUpdates = booleanPreference("manager_auto_updates", false)
|
||||
val showManagerUpdateDialogOnLaunch = booleanPreference("show_manager_update_dialog_on_launch", true)
|
||||
val useManagerPrereleases = booleanPreference("manager_prereleases", false)
|
||||
val usePatchesPrereleases = booleanPreference("patches_prereleases", false)
|
||||
|
||||
val disablePatchVersionCompatCheck = booleanPreference("disable_patch_version_compatibility_check", false)
|
||||
val disableSelectionWarning = booleanPreference("disable_selection_warning", false)
|
||||
val disableUniversalPatchCheck = booleanPreference("disable_patch_universal_check", false)
|
||||
val disableUniversalPatchWarning = booleanPreference("disable_universal_patch_warning", false)
|
||||
val suggestedVersionSafeguard = booleanPreference("suggested_version_safeguard", true)
|
||||
|
||||
val acknowledgedDownloaderPlugins = stringSetPreference("acknowledged_downloader_plugins", emptySet())
|
||||
|
||||
val showDeveloperSettings = booleanPreference("show_developer_settings", context.isDebuggable)
|
||||
}
|
||||
|
||||
@@ -30,8 +30,6 @@ class DownloadedAppRepository(
|
||||
|
||||
fun getAll() = dao.getAllApps().distinctUntilChanged()
|
||||
|
||||
fun get(packageName: String) = dao.get(packageName)
|
||||
|
||||
fun getApkFileForApp(app: DownloadedApp): File =
|
||||
getApkFileForDir(dir.resolve(app.directory))
|
||||
|
||||
@@ -42,8 +40,6 @@ class DownloadedAppRepository(
|
||||
data: Parcelable,
|
||||
expectedPackageName: String,
|
||||
expectedVersion: String?,
|
||||
appCompatibilityCheck: Boolean,
|
||||
patchesCompatibilityCheck: Boolean,
|
||||
onDownload: suspend (downloadProgress: Pair<Long, Long?>) -> Unit,
|
||||
): File {
|
||||
// Converted integers cannot contain / or .. unlike the package name or version, so they are safer to use here.
|
||||
@@ -100,12 +96,7 @@ class DownloadedAppRepository(
|
||||
val pkgInfo =
|
||||
pm.getPackageInfo(targetFile.toFile()) ?: error("Downloaded APK file is invalid")
|
||||
if (pkgInfo.packageName != expectedPackageName) error("Downloaded APK has the wrong package name. Expected: $expectedPackageName, Actual: ${pkgInfo.packageName}")
|
||||
expectedVersion?.let {
|
||||
if (
|
||||
pkgInfo.versionName != expectedVersion &&
|
||||
(appCompatibilityCheck || patchesCompatibilityCheck)
|
||||
) error("The selected app version ($pkgInfo.versionName) doesn't match the suggested version. Please use the suggested version ($expectedVersion), or adjust your settings by disabling \"Require suggested app version\" and enabling \"Disable version compatibility check\".")
|
||||
}
|
||||
if (expectedVersion != null && pkgInfo.versionName != expectedVersion) error("Downloaded APK has the wrong version. Expected: $expectedVersion, Actual: ${pkgInfo.versionName}")
|
||||
|
||||
// Delete the previous copy (if present).
|
||||
dao.get(pkgInfo.packageName, pkgInfo.versionName!!)?.directory?.let {
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package app.revanced.manager.domain.repository
|
||||
|
||||
import app.revanced.manager.data.room.AppDatabase
|
||||
import app.revanced.manager.data.room.AppDatabase.Companion.generateUid
|
||||
import app.revanced.manager.data.room.bundles.PatchBundleEntity
|
||||
import app.revanced.manager.data.room.bundles.Source
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
|
||||
class PatchBundlePersistenceRepository(db: AppDatabase) {
|
||||
private val dao = db.patchBundleDao()
|
||||
|
||||
suspend fun loadConfiguration(): List<PatchBundleEntity> {
|
||||
val all = dao.all()
|
||||
if (all.isEmpty()) {
|
||||
dao.add(defaultSource)
|
||||
return listOf(defaultSource)
|
||||
}
|
||||
|
||||
return all
|
||||
}
|
||||
|
||||
suspend fun reset() = dao.reset()
|
||||
|
||||
suspend fun create(name: String, source: Source, autoUpdate: Boolean = false) =
|
||||
PatchBundleEntity(
|
||||
uid = generateUid(),
|
||||
name = name,
|
||||
version = null,
|
||||
source = source,
|
||||
autoUpdate = autoUpdate
|
||||
).also {
|
||||
dao.add(it)
|
||||
}
|
||||
|
||||
suspend fun delete(uid: Int) = dao.remove(uid)
|
||||
|
||||
suspend fun updateVersion(uid: Int, version: String?) =
|
||||
dao.updateVersion(uid, version)
|
||||
|
||||
suspend fun setAutoUpdate(uid: Int, value: Boolean) = dao.setAutoUpdate(uid, value)
|
||||
|
||||
suspend fun setName(uid: Int, name: String) = dao.setName(uid, name)
|
||||
|
||||
fun getProps(id: Int) = dao.getPropsById(id).distinctUntilChanged()
|
||||
|
||||
private companion object {
|
||||
val defaultSource = PatchBundleEntity(
|
||||
uid = 0,
|
||||
name = "",
|
||||
version = null,
|
||||
source = Source.API,
|
||||
autoUpdate = false
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -3,90 +3,55 @@ package app.revanced.manager.domain.repository
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import androidx.annotation.StringRes
|
||||
import app.revanced.library.mostCommonCompatibleVersions
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.data.platform.NetworkInfo
|
||||
import app.revanced.manager.data.redux.Action
|
||||
import app.revanced.manager.data.redux.ActionContext
|
||||
import app.revanced.manager.data.redux.Store
|
||||
import app.revanced.manager.data.room.AppDatabase
|
||||
import app.revanced.manager.data.room.AppDatabase.Companion.generateUid
|
||||
import app.revanced.manager.data.room.bundles.PatchBundleEntity
|
||||
import app.revanced.manager.data.room.bundles.PatchBundleProperties
|
||||
import app.revanced.manager.data.room.bundles.Source
|
||||
import app.revanced.manager.domain.bundles.APIPatchBundle
|
||||
import app.revanced.manager.domain.bundles.JsonPatchBundle
|
||||
import app.revanced.manager.data.room.bundles.Source as SourceInfo
|
||||
import app.revanced.manager.domain.bundles.LocalPatchBundle
|
||||
import app.revanced.manager.domain.bundles.RemotePatchBundle
|
||||
import app.revanced.manager.domain.bundles.PatchBundleSource
|
||||
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.isDefault
|
||||
import app.revanced.manager.domain.manager.PreferencesManager
|
||||
import app.revanced.manager.patcher.patch.PatchInfo
|
||||
import app.revanced.manager.patcher.patch.PatchBundle
|
||||
import app.revanced.manager.patcher.patch.PatchBundleInfo
|
||||
import app.revanced.manager.util.PatchSelection
|
||||
import app.revanced.manager.util.simpleMessage
|
||||
import app.revanced.manager.util.flatMapLatestAndCombine
|
||||
import app.revanced.manager.util.tag
|
||||
import app.revanced.manager.util.toast
|
||||
import kotlinx.collections.immutable.*
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import app.revanced.manager.util.uiSafe
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.InputStream
|
||||
import kotlin.collections.joinToString
|
||||
import kotlin.collections.map
|
||||
import kotlin.text.ifEmpty
|
||||
|
||||
class PatchBundleRepository(
|
||||
private val app: Application,
|
||||
private val persistenceRepo: PatchBundlePersistenceRepository,
|
||||
private val networkInfo: NetworkInfo,
|
||||
private val prefs: PreferencesManager,
|
||||
db: AppDatabase,
|
||||
) {
|
||||
private val dao = db.patchBundleDao()
|
||||
private val bundlesDir = app.getDir("patch_bundles", Context.MODE_PRIVATE)
|
||||
|
||||
private val store = Store(CoroutineScope(Dispatchers.Default), State())
|
||||
private val _sources: MutableStateFlow<Map<Int, PatchBundleSource>> =
|
||||
MutableStateFlow(emptyMap())
|
||||
val sources = _sources.map { it.values.toList() }
|
||||
|
||||
val sources = store.state.map { it.sources.values.toList() }
|
||||
val bundles = store.state.map {
|
||||
it.sources.mapNotNull { (uid, src) ->
|
||||
uid to (src.patchBundle ?: return@mapNotNull null)
|
||||
}.toMap()
|
||||
}
|
||||
val bundleInfoFlow = store.state.map { it.info }
|
||||
|
||||
fun scopedBundleInfoFlow(packageName: String, version: String?) = bundleInfoFlow.map {
|
||||
it.map { (_, bundleInfo) ->
|
||||
bundleInfo.forPackage(
|
||||
packageName,
|
||||
version
|
||||
)
|
||||
val bundles = sources.flatMapLatestAndCombine(
|
||||
combiner = {
|
||||
it.mapNotNull { (uid, state) ->
|
||||
val bundle = state.patchBundleOrNull() ?: return@mapNotNull null
|
||||
uid to bundle
|
||||
}.toMap()
|
||||
}
|
||||
) {
|
||||
it.state.map { state -> it.uid to state }
|
||||
}
|
||||
|
||||
val patchCountsFlow = bundleInfoFlow.map { it.mapValues { (_, info) -> info.patches.size } }
|
||||
|
||||
fun suggestedVersions(packageName: String, patchSelection: PatchSelection) =
|
||||
bundleInfoFlow.map {
|
||||
val allPatches = patchSelection.flatMap { (uid, patches) ->
|
||||
val bundle = it[uid] ?: return@flatMap emptyList()
|
||||
bundle.patches.filter { patch -> patches.contains(patch.name) }
|
||||
.map(PatchInfo::toPatcherPatch)
|
||||
}.toSet()
|
||||
|
||||
allPatches.mostCommonCompatibleVersions(countUnusedPatches = true)[packageName]
|
||||
}
|
||||
|
||||
val suggestedVersions = bundleInfoFlow.map {
|
||||
val suggestedVersions = bundles.map {
|
||||
val allPatches =
|
||||
it.values.flatMap { bundle -> bundle.patches.map(PatchInfo::toPatcherPatch) }.toSet()
|
||||
|
||||
@@ -109,100 +74,6 @@ class PatchBundleRepository(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend inline fun dispatchAction(
|
||||
name: String,
|
||||
crossinline block: suspend ActionContext.(current: State) -> State
|
||||
) {
|
||||
store.dispatch(object : Action<State> {
|
||||
override suspend fun ActionContext.execute(current: State) = block(current)
|
||||
override fun toString() = name
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a reload. Do not call this outside of a store action.
|
||||
*/
|
||||
private suspend fun doReload(): State {
|
||||
val entities = loadFromDb().onEach {
|
||||
Log.d(tag, "Bundle: $it")
|
||||
}
|
||||
|
||||
val sources = entities.associate { it.uid to it.load() }.toPersistentMap()
|
||||
|
||||
val hasOutOfDateNames = sources.values.any { it.isNameOutOfDate }
|
||||
if (hasOutOfDateNames) dispatchAction(
|
||||
"Sync names"
|
||||
) { state ->
|
||||
val nameChanges = state.sources.mapNotNull { (_, src) ->
|
||||
if (!src.isNameOutOfDate) return@mapNotNull null
|
||||
val newName = src.patchBundle?.manifestAttributes?.name?.takeIf { it != src.name }
|
||||
?: return@mapNotNull null
|
||||
|
||||
src.uid to newName
|
||||
}
|
||||
val sources = state.sources.toMutableMap()
|
||||
val info = state.info.toMutableMap()
|
||||
nameChanges.forEach { (uid, name) ->
|
||||
updateDb(uid) { it.copy(name = name) }
|
||||
sources[uid] = sources[uid]!!.copy(name = name)
|
||||
info[uid] = info[uid]?.copy(name = name) ?: return@forEach
|
||||
}
|
||||
|
||||
State(sources.toPersistentMap(), info.toPersistentMap())
|
||||
}
|
||||
val info = loadMetadata(sources).toPersistentMap()
|
||||
|
||||
return State(sources, info)
|
||||
}
|
||||
|
||||
suspend fun reload() = dispatchAction("Full reload") {
|
||||
doReload()
|
||||
}
|
||||
|
||||
private suspend fun loadFromDb(): List<PatchBundleEntity> {
|
||||
val all = dao.all()
|
||||
if (all.isEmpty()) {
|
||||
dao.upsert(defaultSource)
|
||||
return listOf(defaultSource)
|
||||
}
|
||||
|
||||
return all
|
||||
}
|
||||
|
||||
private suspend fun loadMetadata(sources: Map<Int, PatchBundleSource>): Map<Int, PatchBundleInfo.Global> {
|
||||
// Map bundles -> sources
|
||||
val map = sources.mapNotNull { (_, src) ->
|
||||
(src.patchBundle ?: return@mapNotNull null) to src
|
||||
}.toMap()
|
||||
|
||||
val metadata = try {
|
||||
PatchBundle.Loader.metadata(map.keys)
|
||||
} catch (error: Throwable) {
|
||||
val uids = map.values.map { it.uid }
|
||||
|
||||
dispatchAction("Mark bundles as failed") { state ->
|
||||
state.copy(sources = state.sources.mutate {
|
||||
uids.forEach { uid ->
|
||||
it[uid] = it[uid]?.copy(error = error) ?: return@forEach
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Log.e(tag, "Failed to load bundles", error)
|
||||
emptyMap()
|
||||
}
|
||||
|
||||
return metadata.entries.associate { (bundle, patches) ->
|
||||
val src = map[bundle]!!
|
||||
src.uid to PatchBundleInfo.Global(
|
||||
src.name,
|
||||
bundle.manifestAttributes?.version,
|
||||
src.uid,
|
||||
patches
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun isVersionAllowed(packageName: String, version: String) =
|
||||
withContext(Dispatchers.Default) {
|
||||
if (!prefs.suggestedVersionSafeguard.get()) return@withContext true
|
||||
@@ -218,211 +89,96 @@ class PatchBundleRepository(
|
||||
|
||||
private fun PatchBundleEntity.load(): PatchBundleSource {
|
||||
val dir = directoryOf(uid)
|
||||
val actualName =
|
||||
name.ifEmpty { app.getString(if (uid == 0) R.string.patches_name_default else R.string.patches_name_fallback) }
|
||||
|
||||
return when (source) {
|
||||
is SourceInfo.Local -> LocalPatchBundle(actualName, uid, null, dir)
|
||||
is SourceInfo.API -> APIPatchBundle(
|
||||
actualName,
|
||||
uid,
|
||||
versionHash,
|
||||
null,
|
||||
dir,
|
||||
SourceInfo.API.SENTINEL,
|
||||
autoUpdate,
|
||||
)
|
||||
|
||||
is SourceInfo.Local -> LocalPatchBundle(name, uid, dir)
|
||||
is SourceInfo.API -> APIPatchBundle(name, uid, dir, SourceInfo.API.SENTINEL)
|
||||
is SourceInfo.Remote -> JsonPatchBundle(
|
||||
actualName,
|
||||
name,
|
||||
uid,
|
||||
versionHash,
|
||||
null,
|
||||
dir,
|
||||
source.url.toString(),
|
||||
autoUpdate,
|
||||
source.url.toString()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun createEntity(name: String, source: Source, autoUpdate: Boolean = false) =
|
||||
PatchBundleEntity(
|
||||
uid = generateUid(),
|
||||
name = name,
|
||||
versionHash = null,
|
||||
source = source,
|
||||
autoUpdate = autoUpdate
|
||||
).also {
|
||||
dao.upsert(it)
|
||||
suspend fun reload() = withContext(Dispatchers.Default) {
|
||||
val entities = persistenceRepo.loadConfiguration().onEach {
|
||||
Log.d(tag, "Bundle: $it")
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a patch bundle in the database. Do not use this outside an action.
|
||||
*/
|
||||
private suspend fun updateDb(
|
||||
uid: Int,
|
||||
block: (PatchBundleProperties) -> PatchBundleProperties
|
||||
) {
|
||||
val previous = dao.getProps(uid)!!
|
||||
val new = block(previous)
|
||||
dao.upsert(
|
||||
PatchBundleEntity(
|
||||
uid = uid,
|
||||
name = new.name,
|
||||
versionHash = new.versionHash,
|
||||
source = new.source,
|
||||
autoUpdate = new.autoUpdate,
|
||||
)
|
||||
)
|
||||
_sources.value = entities.associate {
|
||||
it.uid to it.load()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun reset() = dispatchAction("Reset") { state ->
|
||||
dao.reset()
|
||||
state.sources.keys.forEach { directoryOf(it).deleteRecursively() }
|
||||
doReload()
|
||||
suspend fun reset() = withContext(Dispatchers.Default) {
|
||||
persistenceRepo.reset()
|
||||
_sources.value = emptyMap()
|
||||
bundlesDir.apply {
|
||||
deleteRecursively()
|
||||
mkdirs()
|
||||
}
|
||||
|
||||
reload()
|
||||
}
|
||||
|
||||
suspend fun remove(vararg bundles: PatchBundleSource) =
|
||||
dispatchAction("Remove (${bundles.map { it.uid }.joinToString(",")})") { state ->
|
||||
val sources = state.sources.toMutableMap()
|
||||
val info = state.info.toMutableMap()
|
||||
bundles.forEach {
|
||||
if (it.isDefault) return@forEach
|
||||
suspend fun remove(bundle: PatchBundleSource) = withContext(Dispatchers.Default) {
|
||||
persistenceRepo.delete(bundle.uid)
|
||||
directoryOf(bundle.uid).deleteRecursively()
|
||||
|
||||
dao.remove(it.uid)
|
||||
directoryOf(it.uid).deleteRecursively()
|
||||
sources.remove(it.uid)
|
||||
info.remove(it.uid)
|
||||
_sources.update {
|
||||
it.filterKeys { key ->
|
||||
key != bundle.uid
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
State(sources.toPersistentMap(), info.toPersistentMap())
|
||||
private fun addBundle(patchBundle: PatchBundleSource) =
|
||||
_sources.update { it.toMutableMap().apply { put(patchBundle.uid, patchBundle) } }
|
||||
|
||||
suspend fun createLocal(patches: InputStream) = withContext(Dispatchers.Default) {
|
||||
val uid = persistenceRepo.create("", SourceInfo.Local).uid
|
||||
val bundle = LocalPatchBundle("", uid, directoryOf(uid))
|
||||
|
||||
bundle.replace(patches)
|
||||
addBundle(bundle)
|
||||
}
|
||||
|
||||
suspend fun createRemote(url: String, autoUpdate: Boolean) = withContext(Dispatchers.Default) {
|
||||
val entity = persistenceRepo.create("", SourceInfo.from(url), autoUpdate)
|
||||
addBundle(entity.load())
|
||||
}
|
||||
|
||||
private suspend inline fun <reified T> getBundlesByType() =
|
||||
sources.first().filterIsInstance<T>()
|
||||
|
||||
suspend fun reloadApiBundles() {
|
||||
getBundlesByType<APIPatchBundle>().forEach {
|
||||
it.deleteLocalFiles()
|
||||
}
|
||||
|
||||
suspend fun createLocal(createStream: suspend () -> InputStream) = dispatchAction("Add bundle") {
|
||||
with(createEntity("", SourceInfo.Local).load() as LocalPatchBundle) {
|
||||
try {
|
||||
createStream().use { patches -> replace(patches) }
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.e(tag, "Got exception while importing bundle", e)
|
||||
withContext(Dispatchers.Main) {
|
||||
app.toast(app.getString(R.string.patches_replace_fail, e.simpleMessage()))
|
||||
reload()
|
||||
}
|
||||
|
||||
suspend fun redownloadRemoteBundles() =
|
||||
getBundlesByType<RemotePatchBundle>().forEach { it.downloadLatest() }
|
||||
|
||||
suspend fun updateCheck() =
|
||||
uiSafe(app, R.string.source_download_fail, "Failed to update bundles") {
|
||||
coroutineScope {
|
||||
if (!networkInfo.isSafe()) {
|
||||
Log.d(tag, "Skipping update check because the network is down or metered.")
|
||||
return@coroutineScope
|
||||
}
|
||||
|
||||
deleteLocalFile()
|
||||
}
|
||||
}
|
||||
|
||||
doReload()
|
||||
}
|
||||
|
||||
suspend fun createRemote(url: String, autoUpdate: Boolean) =
|
||||
dispatchAction("Add bundle ($url)") { state ->
|
||||
val src = createEntity("", SourceInfo.from(url), autoUpdate).load() as RemotePatchBundle
|
||||
update(src)
|
||||
state.copy(sources = state.sources.put(src.uid, src))
|
||||
}
|
||||
|
||||
suspend fun reloadApiBundles() = dispatchAction("Reload API bundles") {
|
||||
this@PatchBundleRepository.sources.first().filterIsInstance<APIPatchBundle>().forEach {
|
||||
with(it) { deleteLocalFile() }
|
||||
updateDb(it.uid) { it.copy(versionHash = null) }
|
||||
}
|
||||
|
||||
doReload()
|
||||
}
|
||||
|
||||
suspend fun RemotePatchBundle.setAutoUpdate(value: Boolean) =
|
||||
dispatchAction("Set auto update ($name, $value)") { state ->
|
||||
updateDb(uid) { it.copy(autoUpdate = value) }
|
||||
val newSrc = (state.sources[uid] as? RemotePatchBundle)?.copy(autoUpdate = value)
|
||||
?: return@dispatchAction state
|
||||
|
||||
state.copy(sources = state.sources.put(uid, newSrc))
|
||||
}
|
||||
|
||||
suspend fun update(vararg sources: RemotePatchBundle, showToast: Boolean = false) {
|
||||
val uids = sources.map { it.uid }.toSet()
|
||||
store.dispatch(Update(showToast = showToast) { it.uid in uids })
|
||||
}
|
||||
|
||||
suspend fun redownloadRemoteBundles() = store.dispatch(Update(force = true))
|
||||
|
||||
/**
|
||||
* Updates all bundles that should be automatically updated.
|
||||
*/
|
||||
suspend fun updateCheck() = store.dispatch(Update { it.autoUpdate })
|
||||
|
||||
private inner class Update(
|
||||
private val force: Boolean = false,
|
||||
private val showToast: Boolean = false,
|
||||
private val predicate: (bundle: RemotePatchBundle) -> Boolean = { true },
|
||||
) : Action<State> {
|
||||
private suspend fun toast(@StringRes id: Int, vararg args: Any?) =
|
||||
withContext(Dispatchers.Main) { app.toast(app.getString(id, *args)) }
|
||||
|
||||
override fun toString() = if (force) "Redownload remote bundles" else "Update check"
|
||||
|
||||
override suspend fun ActionContext.execute(
|
||||
current: State
|
||||
) = coroutineScope {
|
||||
if (!networkInfo.isSafe()) {
|
||||
Log.d(tag, "Skipping update check because the network is down or metered.")
|
||||
return@coroutineScope current
|
||||
}
|
||||
|
||||
val updated = current.sources.values
|
||||
.filterIsInstance<RemotePatchBundle>()
|
||||
.filter { predicate(it) }
|
||||
.map {
|
||||
async {
|
||||
Log.d(tag, "Updating patch bundle: ${it.name}")
|
||||
|
||||
val newVersion = with(it) {
|
||||
if (force) downloadLatest() else update()
|
||||
} ?: return@async null
|
||||
|
||||
it to newVersion
|
||||
getBundlesByType<RemotePatchBundle>().forEach {
|
||||
launch {
|
||||
if (!it.getProps().autoUpdate) return@launch
|
||||
Log.d(tag, "Updating patch bundle: ${it.getName()}")
|
||||
it.update()
|
||||
}
|
||||
}
|
||||
.awaitAll()
|
||||
.filterNotNull()
|
||||
.toMap()
|
||||
if (updated.isEmpty()) {
|
||||
if (showToast) toast(R.string.patches_update_unavailable)
|
||||
return@coroutineScope current
|
||||
}
|
||||
|
||||
updated.forEach { (src, newVersionHash) ->
|
||||
val name = src.patchBundle?.manifestAttributes?.name ?: src.name
|
||||
|
||||
updateDb(src.uid) {
|
||||
it.copy(versionHash = newVersionHash, name = name)
|
||||
}
|
||||
}
|
||||
|
||||
if (showToast) toast(R.string.patches_update_success)
|
||||
doReload()
|
||||
}
|
||||
|
||||
override suspend fun catch(exception: Exception) {
|
||||
Log.e(tag, "Failed to update patches", exception)
|
||||
toast(R.string.patches_download_fail, exception.simpleMessage())
|
||||
}
|
||||
}
|
||||
|
||||
data class State(
|
||||
val sources: PersistentMap<Int, PatchBundleSource> = persistentMapOf(),
|
||||
val info: PersistentMap<Int, PatchBundleInfo.Global> = persistentMapOf()
|
||||
)
|
||||
|
||||
private companion object {
|
||||
val defaultSource = PatchBundleEntity(
|
||||
uid = 0,
|
||||
name = "",
|
||||
versionHash = null,
|
||||
source = Source.API,
|
||||
autoUpdate = false
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -76,7 +76,7 @@ class PatchOptionsRepository(db: AppDatabase) {
|
||||
fun getPackagesWithSavedOptions() =
|
||||
dao.getPackagesWithOptions().map(Iterable<String>::toSet).distinctUntilChanged()
|
||||
|
||||
suspend fun resetOptionsForPackage(packageName: String) = dao.resetOptionsForPackage(packageName)
|
||||
suspend fun resetOptionsForPatchBundle(uid: Int) = dao.resetOptionsForPatchBundle(uid)
|
||||
suspend fun clearOptionsForPackage(packageName: String) = dao.clearForPackage(packageName)
|
||||
suspend fun clearOptionsForPatchBundle(uid: Int) = dao.clearForPatchBundle(uid)
|
||||
suspend fun reset() = dao.reset()
|
||||
}
|
||||
@@ -3,8 +3,6 @@ package app.revanced.manager.domain.repository
|
||||
import app.revanced.manager.data.room.AppDatabase
|
||||
import app.revanced.manager.data.room.AppDatabase.Companion.generateUid
|
||||
import app.revanced.manager.data.room.selection.PatchSelection
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class PatchSelectionRepository(db: AppDatabase) {
|
||||
private val dao = db.selectionDao()
|
||||
@@ -16,7 +14,7 @@ class PatchSelectionRepository(db: AppDatabase) {
|
||||
packageName = packageName
|
||||
).also { dao.createSelection(it) }.uid
|
||||
|
||||
suspend fun getSelection(packageName: String): app.revanced.manager.util.PatchSelection =
|
||||
suspend fun getSelection(packageName: String): Map<Int, Set<String>> =
|
||||
dao.getSelectedPatches(packageName).mapValues { it.value.toSet() }
|
||||
|
||||
suspend fun updateSelection(packageName: String, selection: Map<Int, Set<String>>) =
|
||||
@@ -27,15 +25,8 @@ class PatchSelectionRepository(db: AppDatabase) {
|
||||
)
|
||||
})
|
||||
|
||||
fun getPackagesWithSavedSelection() =
|
||||
dao.getPackagesWithSelection().map(Iterable<String>::toSet).distinctUntilChanged()
|
||||
|
||||
suspend fun resetSelectionForPackage(packageName: String) {
|
||||
dao.resetForPackage(packageName)
|
||||
}
|
||||
|
||||
suspend fun resetSelectionForPatchBundle(uid: Int) {
|
||||
dao.resetForPatchBundle(uid)
|
||||
suspend fun clearSelection(packageName: String) {
|
||||
dao.clearForPackage(packageName)
|
||||
}
|
||||
|
||||
suspend fun reset() = dao.reset()
|
||||
@@ -43,7 +34,7 @@ class PatchSelectionRepository(db: AppDatabase) {
|
||||
suspend fun export(bundleUid: Int): SerializedSelection = dao.exportSelection(bundleUid)
|
||||
|
||||
suspend fun import(bundleUid: Int, selection: SerializedSelection) {
|
||||
dao.resetForPatchBundle(bundleUid)
|
||||
dao.clearForPatchBundle(bundleUid)
|
||||
dao.updateSelections(selection.entries.associate { (packageName, patches) ->
|
||||
getOrCreateSelection(bundleUid, packageName) to patches.toSet()
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package app.revanced.manager.network.api
|
||||
|
||||
import app.revanced.manager.BuildConfig
|
||||
import android.os.Build
|
||||
import app.revanced.manager.domain.manager.PreferencesManager
|
||||
import app.revanced.manager.network.dto.ReVancedAsset
|
||||
import app.revanced.manager.network.dto.ReVancedGitRepository
|
||||
@@ -30,12 +30,11 @@ class ReVancedAPI(
|
||||
private suspend inline fun <reified T> request(route: String) = request<T>(apiUrl(), route)
|
||||
|
||||
suspend fun getAppUpdate() =
|
||||
getLatestAppInfo().getOrThrow().takeIf { it.version.removePrefix("v") != BuildConfig.VERSION_NAME }
|
||||
getLatestAppInfo().getOrThrow().takeIf { it.version != Build.VERSION.RELEASE }
|
||||
|
||||
suspend fun getLatestAppInfo() =
|
||||
request<ReVancedAsset>("manager?prerelease=${prefs.useManagerPrereleases.get()}")
|
||||
suspend fun getLatestAppInfo() = request<ReVancedAsset>("manager")
|
||||
|
||||
suspend fun getPatchesUpdate() = request<ReVancedAsset>("patches?prerelease=${prefs.usePatchesPrereleases.get()}")
|
||||
suspend fun getPatchesUpdate() = request<ReVancedAsset>("patches")
|
||||
|
||||
suspend fun getContributors() = request<List<ReVancedGitRepository>>("contributors")
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
/**
|
||||
* A container for [Parcelable] data returned from downloader. Instances of this class can be safely stored in a bundle without needing to set the [ClassLoader].
|
||||
* A container for [Parcelable] data returned from downloaders. Instances of this class can be safely stored in a bundle without needing to set the [ClassLoader].
|
||||
*/
|
||||
class ParceledDownloaderData private constructor(
|
||||
val pluginPackageName: String,
|
||||
|
||||
@@ -8,6 +8,7 @@ import app.revanced.manager.util.tag
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.request.HttpRequestBuilder
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.request.prepareGet
|
||||
import io.ktor.client.request.request
|
||||
import io.ktor.client.statement.bodyAsText
|
||||
@@ -16,6 +17,7 @@ import io.ktor.http.isSuccess
|
||||
import io.ktor.utils.io.ByteReadChannel
|
||||
import io.ktor.utils.io.core.isNotEmpty
|
||||
import io.ktor.utils.io.core.readBytes
|
||||
import it.skrape.core.htmlDocument
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.json.Json
|
||||
@@ -91,5 +93,9 @@ class HttpService(
|
||||
builder: HttpRequestBuilder.() -> Unit
|
||||
) = saveLocation.outputStream().use { streamTo(it, builder) }
|
||||
|
||||
suspend fun getHtml(builder: HttpRequestBuilder.() -> Unit) = htmlDocument(
|
||||
html = http.get(builder).bodyAsText()
|
||||
)
|
||||
|
||||
class HttpException(status: HttpStatusCode) : Exception("Failed to fetch: http status: $status")
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
package app.revanced.manager.patcher
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
|
||||
@Parcelize
|
||||
sealed class ProgressEvent : Parcelable {
|
||||
abstract val stepId: StepId?
|
||||
|
||||
data class Started(override val stepId: StepId) : ProgressEvent()
|
||||
|
||||
data class Progress(
|
||||
override val stepId: StepId,
|
||||
val current: Long? = null,
|
||||
val total: Long? = null,
|
||||
val message: String? = null,
|
||||
) : ProgressEvent()
|
||||
|
||||
data class Completed(
|
||||
override val stepId: StepId,
|
||||
) : ProgressEvent()
|
||||
|
||||
data class Failed(
|
||||
override val stepId: StepId?,
|
||||
val error: RemoteError,
|
||||
) : ProgressEvent()
|
||||
}
|
||||
|
||||
/**
|
||||
* Parcelable wrapper for [ProgressEvent].
|
||||
*
|
||||
* Required because AIDL does not support sealed classes.
|
||||
*/
|
||||
@Parcelize
|
||||
data class ProgressEventParcel(val event: ProgressEvent) : Parcelable
|
||||
|
||||
fun ProgressEventParcel.toEvent(): ProgressEvent = event
|
||||
fun ProgressEvent.toParcel(): ProgressEventParcel = ProgressEventParcel(this)
|
||||
|
||||
@Parcelize
|
||||
sealed class StepId : Parcelable {
|
||||
data object DownloadAPK : StepId()
|
||||
data object LoadPatches : StepId()
|
||||
data object ReadAPK : StepId()
|
||||
data object ExecutePatches : StepId()
|
||||
data class ExecutePatch(val index: Int) : StepId()
|
||||
data object WriteAPK : StepId()
|
||||
data object SignAPK : StepId()
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class RemoteError(
|
||||
val type: String,
|
||||
val message: String?,
|
||||
val stackTrace: String,
|
||||
) : Parcelable
|
||||
|
||||
fun Exception.toRemoteError() = RemoteError(
|
||||
type = this::class.java.name,
|
||||
message = this.message,
|
||||
stackTrace = this.stackTraceToString(),
|
||||
)
|
||||
|
||||
|
||||
inline fun <T> runStep(
|
||||
stepId: StepId,
|
||||
onEvent: (ProgressEvent) -> Unit,
|
||||
block: () -> T,
|
||||
): T = try {
|
||||
onEvent(ProgressEvent.Started(stepId))
|
||||
val value = block()
|
||||
onEvent(ProgressEvent.Completed(stepId))
|
||||
value
|
||||
} catch (error: Exception) {
|
||||
onEvent(ProgressEvent.Failed(stepId, error.toRemoteError()))
|
||||
throw error
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
package app.revanced.manager.patcher
|
||||
|
||||
import android.content.Context
|
||||
import app.revanced.library.ApkUtils.applyTo
|
||||
import app.revanced.manager.patcher.Session.Companion.component1
|
||||
import app.revanced.manager.patcher.Session.Companion.component2
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.patcher.logger.Logger
|
||||
import app.revanced.manager.ui.model.State
|
||||
import app.revanced.patcher.Patcher
|
||||
import app.revanced.patcher.PatcherConfig
|
||||
import app.revanced.patcher.patch.Patch
|
||||
@@ -21,10 +22,15 @@ class Session(
|
||||
cacheDir: String,
|
||||
frameworkDir: String,
|
||||
aaptPath: String,
|
||||
private val androidContext: Context,
|
||||
private val logger: Logger,
|
||||
private val input: File,
|
||||
private val onEvent: (ProgressEvent) -> Unit,
|
||||
private val onPatchCompleted: suspend () -> Unit,
|
||||
private val onProgress: (name: String?, state: State?, message: String?) -> Unit
|
||||
) : Closeable {
|
||||
private fun updateProgress(name: String? = null, state: State? = null, message: String? = null) =
|
||||
onProgress(name, state, message)
|
||||
|
||||
private val tempDir = File(cacheDir).resolve("patcher").also { it.mkdirs() }
|
||||
private val patcher = Patcher(
|
||||
PatcherConfig(
|
||||
@@ -36,68 +42,86 @@ class Session(
|
||||
)
|
||||
|
||||
private suspend fun Patcher.applyPatchesVerbose(selectedPatches: PatchList) {
|
||||
var nextPatchIndex = 0
|
||||
|
||||
updateProgress(
|
||||
name = androidContext.getString(R.string.executing_patch, selectedPatches[nextPatchIndex]),
|
||||
state = State.RUNNING
|
||||
)
|
||||
|
||||
this().collect { (patch, exception) ->
|
||||
val index = selectedPatches.indexOf(patch)
|
||||
if (index == -1) return@collect
|
||||
if (patch !in selectedPatches) return@collect
|
||||
|
||||
if (exception != null) {
|
||||
onEvent(
|
||||
ProgressEvent.Failed(
|
||||
StepId.ExecutePatch(index),
|
||||
exception.toRemoteError(),
|
||||
)
|
||||
updateProgress(
|
||||
name = androidContext.getString(R.string.failed_to_execute_patch, patch.name),
|
||||
state = State.FAILED,
|
||||
message = exception.stackTraceToString()
|
||||
)
|
||||
|
||||
logger.error("${patch.name} failed:")
|
||||
logger.error(exception.stackTraceToString())
|
||||
throw exception
|
||||
}
|
||||
|
||||
onEvent(
|
||||
ProgressEvent.Completed(
|
||||
StepId.ExecutePatch(index),
|
||||
nextPatchIndex++
|
||||
|
||||
onPatchCompleted()
|
||||
|
||||
selectedPatches.getOrNull(nextPatchIndex)?.let { nextPatch ->
|
||||
updateProgress(
|
||||
name = androidContext.getString(R.string.executing_patch, nextPatch.name)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
logger.info("${patch.name} succeeded")
|
||||
}
|
||||
|
||||
updateProgress(
|
||||
state = State.COMPLETED,
|
||||
name = androidContext.resources.getQuantityString(
|
||||
R.plurals.patches_executed,
|
||||
selectedPatches.size,
|
||||
selectedPatches.size
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun run(output: File, selectedPatches: PatchList) {
|
||||
runStep(StepId.ExecutePatches, onEvent) {
|
||||
java.util.logging.Logger.getLogger("").apply {
|
||||
handlers.forEach {
|
||||
it.close()
|
||||
removeHandler(it)
|
||||
}
|
||||
updateProgress(state = State.COMPLETED) // Unpacking
|
||||
|
||||
addHandler(logger.handler)
|
||||
java.util.logging.Logger.getLogger("").apply {
|
||||
handlers.forEach {
|
||||
it.close()
|
||||
removeHandler(it)
|
||||
}
|
||||
|
||||
with(patcher) {
|
||||
logger.info("Merging integrations")
|
||||
this += selectedPatches.toSet()
|
||||
|
||||
logger.info("Applying patches...")
|
||||
applyPatchesVerbose(selectedPatches.sortedBy { it.name })
|
||||
}
|
||||
addHandler(logger.handler)
|
||||
}
|
||||
|
||||
runStep(StepId.WriteAPK, onEvent) {
|
||||
logger.info("Writing patched files...")
|
||||
val result = patcher.get()
|
||||
with(patcher) {
|
||||
logger.info("Merging integrations")
|
||||
this += selectedPatches.toSet()
|
||||
|
||||
val patched = tempDir.resolve("result.apk")
|
||||
withContext(Dispatchers.IO) {
|
||||
Files.copy(input.toPath(), patched.toPath(), StandardCopyOption.REPLACE_EXISTING)
|
||||
}
|
||||
result.applyTo(patched)
|
||||
|
||||
logger.info("Patched apk saved to $patched")
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
Files.move(patched.toPath(), output.toPath(), StandardCopyOption.REPLACE_EXISTING)
|
||||
}
|
||||
logger.info("Applying patches...")
|
||||
applyPatchesVerbose(selectedPatches.sortedBy { it.name })
|
||||
}
|
||||
|
||||
logger.info("Writing patched files...")
|
||||
val result = patcher.get()
|
||||
|
||||
val patched = tempDir.resolve("result.apk")
|
||||
withContext(Dispatchers.IO) {
|
||||
Files.copy(input.toPath(), patched.toPath(), StandardCopyOption.REPLACE_EXISTING)
|
||||
}
|
||||
result.applyTo(patched)
|
||||
|
||||
logger.info("Patched apk saved to $patched")
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
Files.move(patched.toPath(), output.toPath(), StandardCopyOption.REPLACE_EXISTING)
|
||||
}
|
||||
updateProgress(state = State.COMPLETED) // Saving
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
|
||||
@@ -1,84 +1,56 @@
|
||||
package app.revanced.manager.patcher.patch
|
||||
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import android.os.Parcelable
|
||||
import app.revanced.patcher.patch.loadPatchesFromDex
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import android.util.Log
|
||||
import app.revanced.manager.util.tag
|
||||
import app.revanced.patcher.patch.Patch
|
||||
import app.revanced.patcher.patch.PatchLoader
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.util.jar.JarFile
|
||||
import kotlin.collections.filter
|
||||
|
||||
@Parcelize
|
||||
data class PatchBundle(val patchesJar: String) : Parcelable {
|
||||
class PatchBundle(val patchesJar: File) {
|
||||
private val loader = object : Iterable<Patch<*>> {
|
||||
private fun load(): Iterable<Patch<*>> {
|
||||
patchesJar.setReadOnly()
|
||||
return PatchLoader.Dex(setOf(patchesJar))
|
||||
}
|
||||
|
||||
override fun iterator(): Iterator<Patch<*>> = load().iterator()
|
||||
}
|
||||
|
||||
init {
|
||||
Log.d(tag, "Loaded patch bundle: $patchesJar")
|
||||
}
|
||||
|
||||
/**
|
||||
* A list containing the metadata of every patch inside this bundle.
|
||||
*/
|
||||
val patches = loader.map(::PatchInfo)
|
||||
|
||||
/**
|
||||
* The [java.util.jar.Manifest] of [patchesJar].
|
||||
*/
|
||||
@IgnoredOnParcel
|
||||
private val manifest by lazy {
|
||||
try {
|
||||
JarFile(patchesJar).use { it.manifest }
|
||||
} catch (_: IOException) {
|
||||
null
|
||||
private val manifest = try {
|
||||
JarFile(patchesJar).use { it.manifest }
|
||||
} catch (_: IOException) {
|
||||
null
|
||||
}
|
||||
|
||||
fun readManifestAttribute(name: String) = manifest?.mainAttributes?.getValue(name)
|
||||
|
||||
/**
|
||||
* Load all patches compatible with the specified package.
|
||||
*/
|
||||
fun patches(packageName: String) = loader.filter { patch ->
|
||||
val compatiblePackages = patch.compatiblePackages
|
||||
?: // The patch has no compatibility constraints, which means it is universal.
|
||||
return@filter true
|
||||
|
||||
if (!compatiblePackages.any { (name, _) -> name == packageName }) {
|
||||
// Patch is not compatible with this package.
|
||||
return@filter false
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
@IgnoredOnParcel
|
||||
val manifestAttributes by lazy {
|
||||
if (manifest != null)
|
||||
ManifestAttributes(
|
||||
name = readManifestAttribute("name"),
|
||||
version = readManifestAttribute("version"),
|
||||
description = readManifestAttribute("description"),
|
||||
source = readManifestAttribute("source"),
|
||||
author = readManifestAttribute("author"),
|
||||
contact = readManifestAttribute("contact"),
|
||||
website = readManifestAttribute("website"),
|
||||
license = readManifestAttribute("license")
|
||||
) else
|
||||
null
|
||||
}
|
||||
|
||||
private fun readManifestAttribute(name: String) = manifest?.mainAttributes?.getValue(name)
|
||||
?.takeIf { it.isNotBlank() } // If empty, set it to null instead.
|
||||
|
||||
data class ManifestAttributes(
|
||||
val name: String?,
|
||||
val version: String?,
|
||||
val description: String?,
|
||||
val source: String?,
|
||||
val author: String?,
|
||||
val contact: String?,
|
||||
val website: String?,
|
||||
val license: String?
|
||||
)
|
||||
|
||||
object Loader {
|
||||
private fun patches(bundles: Iterable<PatchBundle>) =
|
||||
loadPatchesFromDex(
|
||||
bundles.map { File(it.patchesJar) }.toSet()
|
||||
).byPatchesFile.mapKeys { (file, _) ->
|
||||
val absPath = file.absolutePath
|
||||
bundles.single { absPath == it.patchesJar }
|
||||
}
|
||||
|
||||
fun metadata(bundles: Iterable<PatchBundle>) =
|
||||
patches(bundles).mapValues { (_, patches) -> patches.map(::PatchInfo) }
|
||||
|
||||
fun patches(bundles: Iterable<PatchBundle>, packageName: String) =
|
||||
patches(bundles).mapValues { (_, patches) ->
|
||||
patches.filter { patch ->
|
||||
val compatiblePackages = patch.compatiblePackages
|
||||
?: // The patch has no compatibility constraints, which means it is universal.
|
||||
return@filter true
|
||||
|
||||
if (!compatiblePackages.any { (name, _) -> name == packageName }) {
|
||||
// Patch is not compatible with this package.
|
||||
return@filter false
|
||||
}
|
||||
|
||||
true
|
||||
}.toSet()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
package app.revanced.manager.patcher.patch
|
||||
|
||||
import app.revanced.manager.util.PatchSelection
|
||||
|
||||
/**
|
||||
* A base class for storing [PatchBundle] metadata.
|
||||
*/
|
||||
sealed class PatchBundleInfo {
|
||||
/**
|
||||
* The name of the bundle.
|
||||
*/
|
||||
abstract val name: String
|
||||
|
||||
/**
|
||||
* The version of the bundle.
|
||||
*/
|
||||
abstract val version: String?
|
||||
|
||||
/**
|
||||
* The unique ID of the bundle.
|
||||
*/
|
||||
abstract val uid: Int
|
||||
|
||||
/**
|
||||
* The patch list.
|
||||
*/
|
||||
abstract val patches: List<PatchInfo>
|
||||
|
||||
/**
|
||||
* Information about a bundle and all the patches it contains.
|
||||
*
|
||||
* @see [PatchBundleInfo]
|
||||
*/
|
||||
data class Global(
|
||||
override val name: String,
|
||||
override val version: String?,
|
||||
override val uid: Int,
|
||||
override val patches: List<PatchInfo>
|
||||
) : PatchBundleInfo() {
|
||||
/**
|
||||
* Create a [PatchBundleInfo.Scoped] that only contains information about patches that are relevant for a specific [packageName].
|
||||
*/
|
||||
fun forPackage(packageName: String, version: String?): Scoped {
|
||||
val relevantPatches = patches.filter { it.compatibleWith(packageName) }
|
||||
val compatible = mutableListOf<PatchInfo>()
|
||||
val incompatible = mutableListOf<PatchInfo>()
|
||||
val universal = mutableListOf<PatchInfo>()
|
||||
|
||||
relevantPatches.forEach {
|
||||
val targetList = when {
|
||||
it.compatiblePackages == null -> universal
|
||||
it.supports(
|
||||
packageName,
|
||||
version
|
||||
) -> compatible
|
||||
|
||||
else -> incompatible
|
||||
}
|
||||
|
||||
targetList.add(it)
|
||||
}
|
||||
|
||||
return Scoped(
|
||||
name,
|
||||
this.version,
|
||||
uid,
|
||||
relevantPatches,
|
||||
compatible,
|
||||
incompatible,
|
||||
universal
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Contains information about a bundle that is relevant for a specific package name.
|
||||
*
|
||||
* @param compatible Patches that are compatible with the specified package name and version.
|
||||
* @param incompatible Patches that are compatible with the specified package name but not version.
|
||||
* @param universal Patches that are compatible with all packages.
|
||||
* @see [PatchBundleInfo.Global.forPackage]
|
||||
* @see [PatchBundleInfo]
|
||||
*/
|
||||
data class Scoped(
|
||||
override val name: String,
|
||||
override val version: String?,
|
||||
override val uid: Int,
|
||||
override val patches: List<PatchInfo>,
|
||||
val compatible: List<PatchInfo>,
|
||||
val incompatible: List<PatchInfo>,
|
||||
val universal: List<PatchInfo>
|
||||
) : PatchBundleInfo() {
|
||||
fun patchSequence(allowIncompatible: Boolean) = if (allowIncompatible) {
|
||||
patches.asSequence()
|
||||
} else {
|
||||
sequence {
|
||||
yieldAll(compatible)
|
||||
yieldAll(universal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object Extensions {
|
||||
inline fun Iterable<Scoped>.toPatchSelection(
|
||||
allowIncompatible: Boolean,
|
||||
condition: (Int, PatchInfo) -> Boolean
|
||||
): PatchSelection = this.associate { bundle ->
|
||||
val patches =
|
||||
bundle.patchSequence(allowIncompatible)
|
||||
.mapNotNullTo(mutableSetOf()) { patch ->
|
||||
patch.name.takeIf {
|
||||
condition(
|
||||
bundle.uid,
|
||||
patch
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
bundle.uid to patches
|
||||
}
|
||||
|
||||
/**
|
||||
* Algorithm for determining whether all required options have been set.
|
||||
*/
|
||||
inline fun Iterable<Scoped>.requiredOptionsSet(
|
||||
allowIncompatible: Boolean,
|
||||
crossinline isSelected: (Scoped, PatchInfo) -> Boolean,
|
||||
crossinline optionsForPatch: (Scoped, PatchInfo) -> Map<String, Any?>?
|
||||
) = all bundle@{ bundle ->
|
||||
bundle
|
||||
.patchSequence(allowIncompatible)
|
||||
.filter { isSelected(bundle, it) }
|
||||
.all patch@{
|
||||
if (it.options.isNullOrEmpty()) return@patch true
|
||||
val opts by lazy { optionsForPatch(bundle, it).orEmpty() }
|
||||
|
||||
it.options.all option@{ option ->
|
||||
if (!option.required || option.default != null) return@option true
|
||||
|
||||
option.key in opts
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,7 @@ data class PatchInfo(
|
||||
if (pkg.packageName != packageName) return@any false
|
||||
if (pkg.versions == null) return@any true
|
||||
|
||||
versionName == null || versionName in pkg.versions
|
||||
versionName != null && versionName in pkg.versions
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
package app.revanced.manager.patcher.runtime
|
||||
|
||||
import android.content.Context
|
||||
import app.revanced.manager.patcher.ProgressEvent
|
||||
import app.revanced.manager.patcher.Session
|
||||
import app.revanced.manager.patcher.StepId
|
||||
import app.revanced.manager.patcher.logger.Logger
|
||||
import app.revanced.manager.patcher.patch.PatchBundle
|
||||
import app.revanced.manager.patcher.runStep
|
||||
import app.revanced.manager.patcher.worker.ProgressEventHandler
|
||||
import app.revanced.manager.ui.model.State
|
||||
import app.revanced.manager.util.Options
|
||||
import app.revanced.manager.util.PatchSelection
|
||||
import java.io.File
|
||||
@@ -14,7 +12,7 @@ import java.io.File
|
||||
/**
|
||||
* Simple [Runtime] implementation that runs the patcher using coroutines.
|
||||
*/
|
||||
class CoroutineRuntime(context: Context) : Runtime(context) {
|
||||
class CoroutineRuntime(private val context: Context) : Runtime(context) {
|
||||
override suspend fun execute(
|
||||
inputFile: String,
|
||||
outputFile: String,
|
||||
@@ -22,50 +20,44 @@ class CoroutineRuntime(context: Context) : Runtime(context) {
|
||||
selectedPatches: PatchSelection,
|
||||
options: Options,
|
||||
logger: Logger,
|
||||
onEvent: (ProgressEvent) -> Unit,
|
||||
onPatchCompleted: suspend () -> Unit,
|
||||
onProgress: ProgressEventHandler,
|
||||
) {
|
||||
val patchList = runStep(StepId.LoadPatches, onEvent) {
|
||||
val selectedBundles = selectedPatches.keys
|
||||
val bundles = bundles()
|
||||
val uids = bundles.entries.associate { (key, value) -> value to key }
|
||||
val bundles = bundles()
|
||||
|
||||
val allPatches =
|
||||
PatchBundle.Loader.patches(bundles.values, packageName)
|
||||
.mapKeys { (b, _) -> uids[b]!! }
|
||||
.filterKeys { it in selectedBundles }
|
||||
val selectedBundles = selectedPatches.keys
|
||||
val allPatches = bundles.filterKeys { selectedBundles.contains(it) }
|
||||
.mapValues { (_, bundle) -> bundle.patches(packageName) }
|
||||
|
||||
val patchList = selectedPatches.flatMap { (bundle, selected) ->
|
||||
allPatches[bundle]?.filter { it.name in selected }
|
||||
?: throw IllegalArgumentException("Patch bundle $bundle does not exist")
|
||||
}
|
||||
val patchList = selectedPatches.flatMap { (bundle, selected) ->
|
||||
allPatches[bundle]?.filter { selected.contains(it.name) }
|
||||
?: throw IllegalArgumentException("Patch bundle $bundle does not exist")
|
||||
}
|
||||
|
||||
// Set all patch options.
|
||||
options.forEach { (bundle, bundlePatchOptions) ->
|
||||
val patches = allPatches[bundle] ?: return@forEach
|
||||
bundlePatchOptions.forEach { (patchName, configuredPatchOptions) ->
|
||||
val patchOptions = patches.single { it.name == patchName }.options
|
||||
configuredPatchOptions.forEach { (key, value) ->
|
||||
patchOptions[key] = value
|
||||
}
|
||||
// Set all patch options.
|
||||
options.forEach { (bundle, bundlePatchOptions) ->
|
||||
val patches = allPatches[bundle] ?: return@forEach
|
||||
bundlePatchOptions.forEach { (patchName, configuredPatchOptions) ->
|
||||
val patchOptions = patches.single { it.name == patchName }.options
|
||||
configuredPatchOptions.forEach { (key, value) ->
|
||||
patchOptions[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
patchList
|
||||
}
|
||||
|
||||
val session = runStep(StepId.ReadAPK, onEvent) {
|
||||
Session(
|
||||
cacheDir,
|
||||
frameworkPath,
|
||||
aaptPath,
|
||||
logger,
|
||||
File(inputFile),
|
||||
onEvent,
|
||||
)
|
||||
}
|
||||
onProgress(null, State.COMPLETED, null) // Loading patches
|
||||
|
||||
session.use { s ->
|
||||
s.run(
|
||||
Session(
|
||||
cacheDir,
|
||||
frameworkPath,
|
||||
aaptPath,
|
||||
context,
|
||||
logger,
|
||||
File(inputFile),
|
||||
onPatchCompleted = onPatchCompleted,
|
||||
onProgress
|
||||
).use { session ->
|
||||
session.run(
|
||||
File(outputFile),
|
||||
patchList
|
||||
)
|
||||
|
||||
@@ -10,13 +10,12 @@ import app.revanced.manager.BuildConfig
|
||||
import app.revanced.manager.patcher.runtime.process.IPatcherEvents
|
||||
import app.revanced.manager.patcher.runtime.process.IPatcherProcess
|
||||
import app.revanced.manager.patcher.LibraryResolver
|
||||
import app.revanced.manager.patcher.ProgressEvent
|
||||
import app.revanced.manager.patcher.ProgressEventParcel
|
||||
import app.revanced.manager.patcher.logger.Logger
|
||||
import app.revanced.manager.patcher.runtime.process.Parameters
|
||||
import app.revanced.manager.patcher.runtime.process.PatchConfiguration
|
||||
import app.revanced.manager.patcher.runtime.process.PatcherProcess
|
||||
import app.revanced.manager.patcher.toEvent
|
||||
import app.revanced.manager.patcher.worker.ProgressEventHandler
|
||||
import app.revanced.manager.ui.model.State
|
||||
import app.revanced.manager.util.Options
|
||||
import app.revanced.manager.util.PM
|
||||
import app.revanced.manager.util.PatchSelection
|
||||
@@ -67,7 +66,8 @@ class ProcessRuntime(private val context: Context) : Runtime(context) {
|
||||
selectedPatches: PatchSelection,
|
||||
options: Options,
|
||||
logger: Logger,
|
||||
onEvent: (ProgressEvent) -> Unit,
|
||||
onPatchCompleted: suspend () -> Unit,
|
||||
onProgress: ProgressEventHandler,
|
||||
) = coroutineScope {
|
||||
// Get the location of our own Apk.
|
||||
val managerBaseApk = pm.getPackageInfo(context.packageName)!!.applicationInfo!!.sourceDir
|
||||
@@ -123,10 +123,13 @@ class ProcessRuntime(private val context: Context) : Runtime(context) {
|
||||
val eventHandler = object : IPatcherEvents.Stub() {
|
||||
override fun log(level: String, msg: String) = logger.log(enumValueOf(level), msg)
|
||||
|
||||
override fun event(event: ProgressEventParcel?) {
|
||||
event?.let { onEvent(it.toEvent()) }
|
||||
override fun patchSucceeded() {
|
||||
launch { onPatchCompleted() }
|
||||
}
|
||||
|
||||
override fun progress(name: String?, state: String?, msg: String?) =
|
||||
onProgress(name, state?.let { enumValueOf<State>(it) }, msg)
|
||||
|
||||
override fun finished(exceptionStackTrace: String?) {
|
||||
binder.exit()
|
||||
|
||||
@@ -138,6 +141,8 @@ class ProcessRuntime(private val context: Context) : Runtime(context) {
|
||||
}
|
||||
}
|
||||
|
||||
val bundles = bundles()
|
||||
|
||||
val parameters = Parameters(
|
||||
aaptPath = aaptPath,
|
||||
frameworkDir = frameworkPath,
|
||||
@@ -145,11 +150,13 @@ class ProcessRuntime(private val context: Context) : Runtime(context) {
|
||||
packageName = packageName,
|
||||
inputFile = inputFile,
|
||||
outputFile = outputFile,
|
||||
configurations = bundles().map { (uid, bundle) ->
|
||||
configurations = selectedPatches.map { (id, patches) ->
|
||||
val bundle = bundles[id]!!
|
||||
|
||||
PatchConfiguration(
|
||||
bundle,
|
||||
selectedPatches[uid].orEmpty(),
|
||||
options[uid].orEmpty()
|
||||
bundle.patchesJar.absolutePath,
|
||||
patches,
|
||||
options[id].orEmpty()
|
||||
)
|
||||
}
|
||||
)
|
||||
@@ -172,7 +179,7 @@ class ProcessRuntime(private val context: Context) : Runtime(context) {
|
||||
}
|
||||
|
||||
/**
|
||||
* An [Exception] occurred in the remote process while patching.
|
||||
* An [Exception] occured in the remote process while patching.
|
||||
*
|
||||
* @param originalStackTrace The stack trace of the original [Exception].
|
||||
*/
|
||||
|
||||
@@ -4,9 +4,9 @@ import android.content.Context
|
||||
import app.revanced.manager.data.platform.Filesystem
|
||||
import app.revanced.manager.domain.manager.PreferencesManager
|
||||
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||
import app.revanced.manager.patcher.ProgressEvent
|
||||
import app.revanced.manager.patcher.aapt.Aapt
|
||||
import app.revanced.manager.patcher.logger.Logger
|
||||
import app.revanced.manager.patcher.worker.ProgressEventHandler
|
||||
import app.revanced.manager.util.Options
|
||||
import app.revanced.manager.util.PatchSelection
|
||||
import kotlinx.coroutines.flow.first
|
||||
@@ -34,6 +34,7 @@ sealed class Runtime(context: Context) : KoinComponent {
|
||||
selectedPatches: PatchSelection,
|
||||
options: Options,
|
||||
logger: Logger,
|
||||
onEvent: (ProgressEvent) -> Unit,
|
||||
onPatchCompleted: suspend () -> Unit,
|
||||
onProgress: ProgressEventHandler,
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package app.revanced.manager.patcher.runtime.process
|
||||
|
||||
import android.os.Parcelable
|
||||
import app.revanced.manager.patcher.patch.PatchBundle
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import kotlinx.parcelize.RawValue
|
||||
|
||||
@@ -18,7 +17,7 @@ data class Parameters(
|
||||
|
||||
@Parcelize
|
||||
data class PatchConfiguration(
|
||||
val bundle: PatchBundle,
|
||||
val bundlePath: String,
|
||||
val patches: Set<String>,
|
||||
val options: @RawValue Map<String, Map<String, Any?>>
|
||||
) : Parcelable
|
||||
@@ -1,22 +1,17 @@
|
||||
package app.revanced.manager.patcher.runtime.process
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.ActivityThread
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Looper
|
||||
import app.revanced.manager.BuildConfig
|
||||
import app.revanced.manager.patcher.ProgressEvent
|
||||
import app.revanced.manager.patcher.Session
|
||||
import app.revanced.manager.patcher.StepId
|
||||
import app.revanced.manager.patcher.logger.LogLevel
|
||||
import app.revanced.manager.patcher.logger.Logger
|
||||
import app.revanced.manager.patcher.patch.PatchBundle
|
||||
import app.revanced.manager.patcher.runStep
|
||||
import app.revanced.manager.patcher.runtime.ProcessRuntime
|
||||
import app.revanced.manager.patcher.toParcel
|
||||
import app.revanced.manager.ui.model.State
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -27,7 +22,7 @@ import kotlin.system.exitProcess
|
||||
/**
|
||||
* The main class that runs inside the runner process launched by [ProcessRuntime].
|
||||
*/
|
||||
class PatcherProcess() : IPatcherProcess.Stub() {
|
||||
class PatcherProcess(private val context: Context) : IPatcherProcess.Stub() {
|
||||
private var eventBinder: IPatcherEvents? = null
|
||||
|
||||
private val scope =
|
||||
@@ -49,8 +44,6 @@ class PatcherProcess() : IPatcherProcess.Stub() {
|
||||
override fun exit() = exitProcess(0)
|
||||
|
||||
override fun start(parameters: Parameters, events: IPatcherEvents) {
|
||||
fun onEvent(event: ProgressEvent) = events.event(event.toParcel())
|
||||
|
||||
eventBinder = events
|
||||
|
||||
scope.launch {
|
||||
@@ -61,42 +54,39 @@ class PatcherProcess() : IPatcherProcess.Stub() {
|
||||
|
||||
logger.info("Memory limit: ${Runtime.getRuntime().maxMemory() / (1024 * 1024)}MB")
|
||||
|
||||
val patchList = runStep(StepId.LoadPatches, ::onEvent) {
|
||||
val allPatches = PatchBundle.Loader.patches(
|
||||
parameters.configurations.map { it.bundle },
|
||||
parameters.packageName
|
||||
)
|
||||
val patchList = parameters.configurations.flatMap { config ->
|
||||
val bundle = PatchBundle(File(config.bundlePath))
|
||||
|
||||
parameters.configurations.flatMap { config ->
|
||||
val patches = (allPatches[config.bundle] ?: return@flatMap emptyList())
|
||||
.filter { it.name in config.patches }
|
||||
val patches =
|
||||
bundle.patches(parameters.packageName).filter { it.name in config.patches }
|
||||
.associateBy { it.name }
|
||||
|
||||
config.options.forEach { (patchName, opts) ->
|
||||
val patchOptions = patches[patchName]?.options
|
||||
?: throw Exception("Patch with name $patchName does not exist.")
|
||||
config.options.forEach { (patchName, opts) ->
|
||||
val patchOptions = patches[patchName]?.options
|
||||
?: throw Exception("Patch with name $patchName does not exist.")
|
||||
|
||||
opts.forEach { (key, value) ->
|
||||
patchOptions[key] = value
|
||||
}
|
||||
opts.forEach { (key, value) ->
|
||||
patchOptions[key] = value
|
||||
}
|
||||
|
||||
patches.values
|
||||
}
|
||||
|
||||
patches.values
|
||||
}
|
||||
|
||||
val session = runStep(StepId.ReadAPK, ::onEvent) {
|
||||
Session(
|
||||
cacheDir = parameters.cacheDir,
|
||||
aaptPath = parameters.aaptPath,
|
||||
frameworkDir = parameters.frameworkDir,
|
||||
logger = logger,
|
||||
input = File(parameters.inputFile),
|
||||
onEvent = ::onEvent,
|
||||
)
|
||||
}
|
||||
events.progress(null, State.COMPLETED.name, null) // Loading patches
|
||||
|
||||
session.use {
|
||||
Session(
|
||||
cacheDir = parameters.cacheDir,
|
||||
aaptPath = parameters.aaptPath,
|
||||
frameworkDir = parameters.frameworkDir,
|
||||
androidContext = context,
|
||||
logger = logger,
|
||||
input = File(parameters.inputFile),
|
||||
onPatchCompleted = { events.patchSucceeded() },
|
||||
onProgress = { name, state, message ->
|
||||
events.progress(name, state?.name, message)
|
||||
}
|
||||
).use {
|
||||
it.run(File(parameters.outputFile), patchList)
|
||||
}
|
||||
|
||||
@@ -105,10 +95,6 @@ class PatcherProcess() : IPatcherProcess.Stub() {
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val longArrayClass = LongArray::class.java
|
||||
private val emptyLongArray = LongArray(0)
|
||||
|
||||
@SuppressLint("PrivateApi")
|
||||
@JvmStatic
|
||||
fun main(args: Array<String>) {
|
||||
Looper.prepare()
|
||||
@@ -119,16 +105,7 @@ class PatcherProcess() : IPatcherProcess.Stub() {
|
||||
val systemContext = ActivityThread.systemMain().systemContext as Context
|
||||
val appContext = systemContext.createPackageContext(managerPackageName, 0)
|
||||
|
||||
// Avoid annoying logs. See https://github.com/robolectric/robolectric/blob/ad0484c6b32c7d11176c711abeb3cb4a900f9258/robolectric/src/main/java/org/robolectric/android/internal/AndroidTestEnvironment.java#L376-L388
|
||||
Class.forName("android.app.AppCompatCallbacks").apply {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
|
||||
getDeclaredMethod("install", longArrayClass, longArrayClass).also { it.isAccessible = true }(null, emptyLongArray, emptyLongArray)
|
||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
getDeclaredMethod("install", longArrayClass).also { it.isAccessible = true }(null, emptyLongArray)
|
||||
}
|
||||
}
|
||||
|
||||
val ipcInterface = PatcherProcess()
|
||||
val ipcInterface = PatcherProcess(appContext)
|
||||
|
||||
appContext.sendBroadcast(Intent().apply {
|
||||
action = ProcessRuntime.CONNECT_TO_APP_ACTION
|
||||
|
||||
@@ -14,9 +14,9 @@ import android.os.Parcelable
|
||||
import android.os.PowerManager
|
||||
import android.util.Log
|
||||
import androidx.activity.result.ActivityResult
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.work.ForegroundInfo
|
||||
import androidx.work.WorkerParameters
|
||||
import app.revanced.manager.MainActivity
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.data.platform.Filesystem
|
||||
import app.revanced.manager.data.room.apps.installed.InstallType
|
||||
@@ -29,17 +29,14 @@ import app.revanced.manager.domain.repository.InstalledAppRepository
|
||||
import app.revanced.manager.domain.worker.Worker
|
||||
import app.revanced.manager.domain.worker.WorkerRepository
|
||||
import app.revanced.manager.network.downloader.LoadedDownloaderPlugin
|
||||
import app.revanced.manager.patcher.ProgressEvent
|
||||
import app.revanced.manager.patcher.StepId
|
||||
import app.revanced.manager.patcher.logger.Logger
|
||||
import app.revanced.manager.patcher.runStep
|
||||
import app.revanced.manager.patcher.runtime.CoroutineRuntime
|
||||
import app.revanced.manager.patcher.runtime.ProcessRuntime
|
||||
import app.revanced.manager.patcher.toRemoteError
|
||||
import app.revanced.manager.plugin.downloader.GetScope
|
||||
import app.revanced.manager.plugin.downloader.PluginHostApi
|
||||
import app.revanced.manager.plugin.downloader.UserInteractionException
|
||||
import app.revanced.manager.ui.model.SelectedSource
|
||||
import app.revanced.manager.ui.model.SelectedApp
|
||||
import app.revanced.manager.ui.model.State
|
||||
import app.revanced.manager.util.Options
|
||||
import app.revanced.manager.util.PM
|
||||
import app.revanced.manager.util.PatchSelection
|
||||
@@ -51,6 +48,8 @@ import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import java.io.File
|
||||
|
||||
typealias ProgressEventHandler = (name: String?, state: State?, message: String?) -> Unit
|
||||
|
||||
@OptIn(PluginHostApi::class)
|
||||
class PatcherWorker(
|
||||
context: Context,
|
||||
@@ -67,17 +66,19 @@ class PatcherWorker(
|
||||
private val rootInstaller: RootInstaller by inject()
|
||||
|
||||
class Args(
|
||||
val packageName: String,
|
||||
val version: String?,
|
||||
val source: SelectedSource,
|
||||
val input: SelectedApp,
|
||||
val output: String,
|
||||
val selectedPatches: PatchSelection,
|
||||
val options: Options,
|
||||
val logger: Logger,
|
||||
val onDownloadProgress: suspend (Pair<Long, Long?>?) -> Unit,
|
||||
val onPatchCompleted: suspend () -> Unit,
|
||||
val handleStartActivityRequest: suspend (LoadedDownloaderPlugin, Intent) -> ActivityResult,
|
||||
val setInputFile: suspend (File) -> Unit,
|
||||
val onEvent: (ProgressEvent) -> Unit,
|
||||
)
|
||||
val onProgress: ProgressEventHandler
|
||||
) {
|
||||
val packageName get() = input.packageName
|
||||
}
|
||||
|
||||
override suspend fun getForegroundInfo() =
|
||||
ForegroundInfo(
|
||||
@@ -87,25 +88,22 @@ class PatcherWorker(
|
||||
)
|
||||
|
||||
private fun createNotification(): Notification {
|
||||
val notificationIntent = Intent(applicationContext, MainActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
}
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
val notificationIntent = Intent(applicationContext, PatcherWorker::class.java)
|
||||
val pendingIntent: PendingIntent = PendingIntent.getActivity(
|
||||
applicationContext, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
val channel = NotificationChannel(
|
||||
"revanced-patcher-patching", "Patching", NotificationManager.IMPORTANCE_LOW
|
||||
"revanced-patcher-patching", "Patching", NotificationManager.IMPORTANCE_HIGH
|
||||
)
|
||||
val notificationManager =
|
||||
applicationContext.getSystemService(NotificationManager::class.java)
|
||||
notificationManager.createNotificationChannel(channel)
|
||||
ContextCompat.getSystemService(applicationContext, NotificationManager::class.java)
|
||||
notificationManager!!.createNotificationChannel(channel)
|
||||
return Notification.Builder(applicationContext, channel.id)
|
||||
.setContentTitle(applicationContext.getText(R.string.patcher_notification_title))
|
||||
.setContentText(applicationContext.getText(R.string.patcher_notification_text))
|
||||
.setContentTitle(applicationContext.getText(R.string.app_name))
|
||||
.setContentText(applicationContext.getText(R.string.patcher_notification_message))
|
||||
.setLargeIcon(Icon.createWithResource(applicationContext, R.drawable.ic_notification))
|
||||
.setSmallIcon(Icon.createWithResource(applicationContext, R.drawable.ic_notification))
|
||||
.setContentIntent(pendingIntent)
|
||||
.setCategory(Notification.CATEGORY_SERVICE)
|
||||
.build()
|
||||
.setContentIntent(pendingIntent).build()
|
||||
}
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
@@ -139,10 +137,14 @@ class PatcherWorker(
|
||||
}
|
||||
|
||||
private suspend fun runPatcher(args: Args): Result {
|
||||
|
||||
fun updateProgress(name: String? = null, state: State? = null, message: String? = null) =
|
||||
args.onProgress(name, state, message)
|
||||
|
||||
val patchedApk = fs.tempDir.resolve("patched.apk")
|
||||
|
||||
return try {
|
||||
if (args.source is SelectedSource.Installed) {
|
||||
if (args.input is SelectedApp.Installed) {
|
||||
installedAppRepository.get(args.packageName)?.let {
|
||||
if (it.installType == InstallType.MOUNT) {
|
||||
rootInstaller.unmount(args.packageName)
|
||||
@@ -155,65 +157,56 @@ class PatcherWorker(
|
||||
plugin,
|
||||
data,
|
||||
args.packageName,
|
||||
args.version,
|
||||
prefs.suggestedVersionSafeguard.get(),
|
||||
!prefs.disablePatchVersionCompatCheck.get(),
|
||||
) { progress ->
|
||||
args.onEvent(
|
||||
ProgressEvent.Progress(
|
||||
stepId = StepId.DownloadAPK,
|
||||
current = progress.first,
|
||||
total = progress.second
|
||||
)
|
||||
)
|
||||
}.also { args.setInputFile(it) }
|
||||
|
||||
val inputFile = when (val source = args.source) {
|
||||
is SelectedSource.Auto -> throw Exception("Auto source is not supported in worker.")
|
||||
|
||||
is SelectedSource.Plugin -> {
|
||||
runStep(StepId.DownloadAPK, args.onEvent) {
|
||||
downloaderPluginRepository.loadedPluginsFlow.first()
|
||||
.firstNotNullOfOrNull { plugin ->
|
||||
try {
|
||||
val getScope = object : GetScope {
|
||||
override val pluginPackageName = plugin.packageName
|
||||
override val hostPackageName =
|
||||
applicationContext.packageName
|
||||
|
||||
override suspend fun requestStartActivity(intent: Intent): Intent? {
|
||||
val result =
|
||||
args.handleStartActivityRequest(plugin, intent)
|
||||
return when (result.resultCode) {
|
||||
Activity.RESULT_OK -> result.data
|
||||
Activity.RESULT_CANCELED -> throw UserInteractionException.Activity.Cancelled()
|
||||
else -> throw UserInteractionException.Activity.NotCompleted(
|
||||
result.resultCode,
|
||||
result.data
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
withContext(Dispatchers.IO) {
|
||||
plugin.get(
|
||||
getScope,
|
||||
args.packageName,
|
||||
args.version
|
||||
)
|
||||
}?.takeIf { (_, version) -> args.version == null || version == args.version }
|
||||
} catch (e: UserInteractionException.Activity.NotCompleted) {
|
||||
throw e
|
||||
} catch (_: UserInteractionException) {
|
||||
null
|
||||
}?.let { (data, _) -> download(plugin, data) }
|
||||
} ?: throw Exception("App is not available.")
|
||||
}
|
||||
args.input.version,
|
||||
onDownload = args.onDownloadProgress
|
||||
).also {
|
||||
args.setInputFile(it)
|
||||
updateProgress(state = State.COMPLETED) // Download APK
|
||||
}
|
||||
|
||||
is SelectedSource.Downloaded -> File(source.path)
|
||||
is SelectedSource.Local -> File(source.path)
|
||||
val inputFile = when (val selectedApp = args.input) {
|
||||
is SelectedApp.Download -> {
|
||||
val (plugin, data) = downloaderPluginRepository.unwrapParceledData(selectedApp.data)
|
||||
|
||||
is SelectedSource.Installed -> File(pm.getPackageInfo(args.packageName)!!.applicationInfo!!.sourceDir)
|
||||
download(plugin, data)
|
||||
}
|
||||
|
||||
is SelectedApp.Search -> {
|
||||
downloaderPluginRepository.loadedPluginsFlow.first()
|
||||
.firstNotNullOfOrNull { plugin ->
|
||||
try {
|
||||
val getScope = object : GetScope {
|
||||
override val pluginPackageName = plugin.packageName
|
||||
override val hostPackageName = applicationContext.packageName
|
||||
override suspend fun requestStartActivity(intent: Intent): Intent? {
|
||||
val result = args.handleStartActivityRequest(plugin, intent)
|
||||
return when (result.resultCode) {
|
||||
Activity.RESULT_OK -> result.data
|
||||
Activity.RESULT_CANCELED -> throw UserInteractionException.Activity.Cancelled()
|
||||
else -> throw UserInteractionException.Activity.NotCompleted(
|
||||
result.resultCode,
|
||||
result.data
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
withContext(Dispatchers.IO) {
|
||||
plugin.get(
|
||||
getScope,
|
||||
selectedApp.packageName,
|
||||
selectedApp.version
|
||||
)
|
||||
}?.takeIf { (_, version) -> selectedApp.version == null || version == selectedApp.version }
|
||||
} catch (e: UserInteractionException.Activity.NotCompleted) {
|
||||
throw e
|
||||
} catch (_: UserInteractionException) {
|
||||
null
|
||||
}?.let { (data, _) -> download(plugin, data) }
|
||||
} ?: throw Exception("App is not available.")
|
||||
}
|
||||
|
||||
is SelectedApp.Local -> selectedApp.file.also { args.setInputFile(it) }
|
||||
is SelectedApp.Installed -> File(pm.getPackageInfo(selectedApp.packageName)!!.applicationInfo!!.sourceDir)
|
||||
}
|
||||
|
||||
val runtime = if (prefs.useProcessRuntime.get()) {
|
||||
@@ -229,12 +222,12 @@ class PatcherWorker(
|
||||
args.selectedPatches,
|
||||
args.options,
|
||||
args.logger,
|
||||
args.onEvent,
|
||||
args.onPatchCompleted,
|
||||
args.onProgress
|
||||
)
|
||||
|
||||
runStep(StepId.SignAPK, args.onEvent) {
|
||||
keystoreManager.sign(patchedApk, File(args.output))
|
||||
}
|
||||
keystoreManager.sign(patchedApk, File(args.output))
|
||||
updateProgress(state = State.COMPLETED) // Signing
|
||||
|
||||
Log.i(tag, "Patching succeeded".logFmt())
|
||||
Result.success()
|
||||
@@ -243,15 +236,17 @@ class PatcherWorker(
|
||||
tag,
|
||||
"An exception occurred in the remote process while patching. ${e.originalStackTrace}".logFmt()
|
||||
)
|
||||
args.onEvent(ProgressEvent.Failed(null, e.toRemoteError())) // Fallback if exception doesn't occur within step
|
||||
updateProgress(state = State.FAILED, message = e.originalStackTrace)
|
||||
Result.failure()
|
||||
} catch (e: Exception) {
|
||||
Log.e(tag, "An exception occurred while patching".logFmt(), e)
|
||||
args.onEvent(ProgressEvent.Failed(null, e.toRemoteError())) // Fallback if exception doesn't occur within step
|
||||
updateProgress(state = State.FAILED, message = e.stackTraceToString())
|
||||
Result.failure()
|
||||
} finally {
|
||||
patchedApk.delete()
|
||||
if (args.source is SelectedSource.Local) File(args.source.path).delete()
|
||||
if (args.input is SelectedApp.Local && args.input.temporary) {
|
||||
args.input.file.delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package app.revanced.manager.service
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageInstaller
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
class InstallService : Service() {
|
||||
|
||||
override fun onStartCommand(
|
||||
intent: Intent, flags: Int, startId: Int
|
||||
): Int {
|
||||
val extraStatus = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -999)
|
||||
val extraStatusMessage = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE)
|
||||
val extraPackageName = intent.getStringExtra(PackageInstaller.EXTRA_PACKAGE_NAME)
|
||||
when (extraStatus) {
|
||||
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
|
||||
startActivity(if (Build.VERSION.SDK_INT >= 33) {
|
||||
intent.getParcelableExtra(Intent.EXTRA_INTENT, Intent::class.java)
|
||||
} else {
|
||||
intent.getParcelableExtra(Intent.EXTRA_INTENT)
|
||||
}.apply {
|
||||
this?.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
})
|
||||
}
|
||||
|
||||
else -> {
|
||||
sendBroadcast(Intent().apply {
|
||||
action = APP_INSTALL_ACTION
|
||||
`package` = packageName
|
||||
putExtra(EXTRA_INSTALL_STATUS, extraStatus)
|
||||
putExtra(EXTRA_INSTALL_STATUS_MESSAGE, extraStatusMessage)
|
||||
putExtra(EXTRA_PACKAGE_NAME, extraPackageName)
|
||||
})
|
||||
}
|
||||
}
|
||||
stopSelf()
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
|
||||
companion object {
|
||||
const val APP_INSTALL_ACTION = "APP_INSTALL_ACTION"
|
||||
|
||||
const val EXTRA_INSTALL_STATUS = "EXTRA_INSTALL_STATUS"
|
||||
const val EXTRA_INSTALL_STATUS_MESSAGE = "EXTRA_INSTALL_STATUS_MESSAGE"
|
||||
const val EXTRA_PACKAGE_NAME = "EXTRA_PACKAGE_NAME"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -15,7 +15,6 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import app.revanced.manager.R
|
||||
import io.github.fornewid.placeholder.material3.placeholder
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -48,8 +47,6 @@ fun AppLabel(
|
||||
shape = RoundedCornerShape(100)
|
||||
)
|
||||
.then(modifier),
|
||||
style = style,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = style
|
||||
)
|
||||
}
|
||||
@@ -81,41 +81,3 @@ fun AppTopBar(
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AppTopBar(
|
||||
title: @Composable () -> Unit,
|
||||
onBackClick: (() -> Unit)? = null,
|
||||
backIcon: @Composable (() -> Unit) = @Composable {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(
|
||||
R.string.back
|
||||
)
|
||||
)
|
||||
},
|
||||
actions: @Composable (RowScope.() -> Unit) = {},
|
||||
scrollBehavior: TopAppBarScrollBehavior? = null,
|
||||
applyContainerColor: Boolean = false
|
||||
) {
|
||||
val containerColor = if (applyContainerColor) {
|
||||
MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp)
|
||||
} else {
|
||||
Color.Unspecified
|
||||
}
|
||||
|
||||
TopAppBar(
|
||||
title = title,
|
||||
scrollBehavior = scrollBehavior,
|
||||
navigationIcon = {
|
||||
if (onBackClick != null) {
|
||||
IconButton(onClick = onBackClick) {
|
||||
backIcon()
|
||||
}
|
||||
}
|
||||
},
|
||||
actions = actions,
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = containerColor
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
package app.revanced.manager.ui.component
|
||||
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import app.revanced.manager.R
|
||||
|
||||
@Composable
|
||||
fun ConfirmDialog(
|
||||
onDismiss: () -> Unit,
|
||||
onConfirm: () -> Unit,
|
||||
title: String,
|
||||
description: String,
|
||||
icon: ImageVector
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
dismissButton = {
|
||||
TextButton(onDismiss) {
|
||||
Text(stringResource(R.string.cancel))
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
onConfirm()
|
||||
onDismiss()
|
||||
}
|
||||
) {
|
||||
Text(stringResource(R.string.confirm))
|
||||
}
|
||||
},
|
||||
title = { Text(title) },
|
||||
icon = { Icon(icon, null) },
|
||||
text = { Text(description) }
|
||||
)
|
||||
}
|
||||
@@ -16,6 +16,8 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.ui.component.bundle.BundleTopBar
|
||||
|
||||
@@ -24,13 +26,17 @@ import app.revanced.manager.ui.component.bundle.BundleTopBar
|
||||
fun ExceptionViewerDialog(text: String, onDismiss: () -> Unit) {
|
||||
val context = LocalContext.current
|
||||
|
||||
FullscreenDialog(
|
||||
Dialog(
|
||||
onDismissRequest = onDismiss,
|
||||
properties = DialogProperties(
|
||||
usePlatformDefaultWidth = false,
|
||||
dismissOnBackPress = true
|
||||
)
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
BundleTopBar(
|
||||
title = stringResource(R.string.patches_error),
|
||||
title = stringResource(R.string.bundle_error),
|
||||
onBackClick = onDismiss,
|
||||
backIcon = {
|
||||
Icon(
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
package app.revanced.manager.ui.component
|
||||
|
||||
import android.view.WindowManager
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.compose.ui.window.DialogWindowProvider
|
||||
import androidx.core.view.WindowCompat
|
||||
|
||||
private val properties = DialogProperties(
|
||||
usePlatformDefaultWidth = false,
|
||||
dismissOnBackPress = true,
|
||||
decorFitsSystemWindows = false,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun FullscreenDialog(onDismissRequest: () -> Unit, content: @Composable () -> Unit) {
|
||||
Dialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
properties = properties
|
||||
) {
|
||||
val view = LocalView.current
|
||||
val isDarkTheme = isSystemInDarkTheme()
|
||||
LaunchedEffect(isDarkTheme) {
|
||||
val window = (view.parent as DialogWindowProvider).window
|
||||
window.statusBarColor = Color.Transparent.toArgb()
|
||||
window.navigationBarColor = Color.Transparent.toArgb()
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS)
|
||||
|
||||
val insetsController = WindowCompat.getInsetsController(window, view)
|
||||
insetsController.isAppearanceLightStatusBars = !isDarkTheme
|
||||
insetsController.isAppearanceLightNavigationBars = !isDarkTheme
|
||||
}
|
||||
|
||||
content()
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
package app.revanced.manager.ui.component
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.pm.PackageInstaller
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.annotation.StringRes
|
||||
@@ -80,7 +79,7 @@ private fun installerStatusDialogButton(
|
||||
enum class DialogKind(
|
||||
val flag: Int,
|
||||
val title: Int,
|
||||
@param:StringRes val contentStringResId: Int,
|
||||
@StringRes val contentStringResId: Int,
|
||||
val icon: ImageVector = Icons.Outlined.ErrorOutline,
|
||||
val confirmButton: InstallerStatusDialogButton = installerStatusDialogButton(R.string.ok),
|
||||
val dismissButton: InstallerStatusDialogButton? = null,
|
||||
@@ -134,8 +133,10 @@ enum class DialogKind(
|
||||
title = R.string.installation_storage_issue_dialog_title,
|
||||
contentStringResId = R.string.installation_storage_issue_description,
|
||||
),
|
||||
|
||||
@RequiresApi(34)
|
||||
FAILURE_TIMEOUT(
|
||||
flag = @SuppressLint("InlinedApi") PackageInstaller.STATUS_FAILURE_TIMEOUT,
|
||||
flag = PackageInstaller.STATUS_FAILURE_TIMEOUT,
|
||||
title = R.string.installation_timeout_dialog_title,
|
||||
contentStringResId = R.string.installation_timeout_description,
|
||||
confirmButton = installerStatusDialogButton(R.string.install_app) { model ->
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
package app.revanced.manager.ui.component.bundle
|
||||
|
||||
import android.webkit.URLUtil
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.ArrowRight
|
||||
import androidx.compose.material.icons.outlined.Extension
|
||||
import androidx.compose.material.icons.outlined.Inventory2
|
||||
import androidx.compose.material.icons.outlined.Sell
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.ui.component.ColumnWithScrollbar
|
||||
import app.revanced.manager.ui.component.TextInputDialog
|
||||
import app.revanced.manager.ui.component.haptics.HapticSwitch
|
||||
|
||||
@Composable
|
||||
fun BaseBundleDialog(
|
||||
modifier: Modifier = Modifier,
|
||||
isDefault: Boolean,
|
||||
name: String?,
|
||||
remoteUrl: String?,
|
||||
onRemoteUrlChange: ((String) -> Unit)? = null,
|
||||
patchCount: Int,
|
||||
version: String?,
|
||||
autoUpdate: Boolean,
|
||||
onAutoUpdateChange: (Boolean) -> Unit,
|
||||
onPatchesClick: () -> Unit,
|
||||
extraFields: @Composable ColumnScope.() -> Unit = {}
|
||||
) {
|
||||
ColumnWithScrollbar(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.then(modifier),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.Start),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Inventory2,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(32.dp)
|
||||
)
|
||||
name?.let {
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight(800)),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
}
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 2.dp)
|
||||
) {
|
||||
version?.let {
|
||||
Tag(Icons.Outlined.Sell, it)
|
||||
}
|
||||
Tag(Icons.Outlined.Extension, patchCount.toString())
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
color = MaterialTheme.colorScheme.outlineVariant
|
||||
)
|
||||
|
||||
if (remoteUrl != null) {
|
||||
BundleListItem(
|
||||
headlineText = stringResource(R.string.bundle_auto_update),
|
||||
supportingText = stringResource(R.string.bundle_auto_update_description),
|
||||
trailingContent = {
|
||||
HapticSwitch(
|
||||
checked = autoUpdate,
|
||||
onCheckedChange = onAutoUpdateChange
|
||||
)
|
||||
},
|
||||
modifier = Modifier.clickable {
|
||||
onAutoUpdateChange(!autoUpdate)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
remoteUrl?.takeUnless { isDefault }?.let { url ->
|
||||
var showUrlInputDialog by rememberSaveable {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
if (showUrlInputDialog) {
|
||||
TextInputDialog(
|
||||
initial = url,
|
||||
title = stringResource(R.string.bundle_input_source_url),
|
||||
onDismissRequest = { showUrlInputDialog = false },
|
||||
onConfirm = {
|
||||
showUrlInputDialog = false
|
||||
onRemoteUrlChange?.invoke(it)
|
||||
},
|
||||
validator = {
|
||||
if (it.isEmpty()) return@TextInputDialog false
|
||||
|
||||
URLUtil.isValidUrl(it)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
BundleListItem(
|
||||
modifier = Modifier.clickable(
|
||||
enabled = onRemoteUrlChange != null,
|
||||
onClick = {
|
||||
showUrlInputDialog = true
|
||||
}
|
||||
),
|
||||
headlineText = stringResource(R.string.bundle_input_source_url),
|
||||
supportingText = url.ifEmpty {
|
||||
stringResource(R.string.field_not_set)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
val patchesClickable = patchCount > 0
|
||||
BundleListItem(
|
||||
headlineText = stringResource(R.string.patches),
|
||||
supportingText = stringResource(R.string.bundle_view_patches),
|
||||
modifier = Modifier.clickable(
|
||||
enabled = patchesClickable,
|
||||
onClick = onPatchesClick
|
||||
)
|
||||
) {
|
||||
if (patchesClickable) {
|
||||
Icon(
|
||||
Icons.AutoMirrored.Outlined.ArrowRight,
|
||||
stringResource(R.string.patches)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extraFields()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Tag(
|
||||
icon: ImageVector,
|
||||
text: String
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp),
|
||||
tint = MaterialTheme.colorScheme.outline,
|
||||
)
|
||||
Text(
|
||||
text,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.outline,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,102 +1,70 @@
|
||||
package app.revanced.manager.ui.component.bundle
|
||||
|
||||
import android.webkit.URLUtil.isValidUrl
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.automirrored.outlined.ArrowRight
|
||||
import androidx.compose.material.icons.automirrored.outlined.Send
|
||||
import androidx.compose.material.icons.outlined.Commit
|
||||
import androidx.compose.material.icons.outlined.DeleteOutline
|
||||
import androidx.compose.material.icons.outlined.Description
|
||||
import androidx.compose.material.icons.outlined.Gavel
|
||||
import androidx.compose.material.icons.outlined.Language
|
||||
import androidx.compose.material.icons.outlined.Person
|
||||
import androidx.compose.material.icons.outlined.Sell
|
||||
import androidx.compose.material.icons.outlined.Update
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.R.string.auto_update
|
||||
import app.revanced.manager.R.string.auto_update_description
|
||||
import app.revanced.manager.R.string.field_not_set
|
||||
import app.revanced.manager.R.string.patches
|
||||
import app.revanced.manager.R.string.patches_url
|
||||
import app.revanced.manager.R.string.view_patches
|
||||
import app.revanced.manager.data.platform.NetworkInfo
|
||||
import app.revanced.manager.domain.bundles.LocalPatchBundle
|
||||
import app.revanced.manager.domain.bundles.PatchBundleSource
|
||||
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.asRemoteOrNull
|
||||
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.isDefault
|
||||
import app.revanced.manager.domain.manager.PreferencesManager
|
||||
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||
import app.revanced.manager.ui.component.ColumnWithScrollbar
|
||||
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.nameState
|
||||
import app.revanced.manager.ui.component.ExceptionViewerDialog
|
||||
import app.revanced.manager.ui.component.FullscreenDialog
|
||||
import app.revanced.manager.ui.component.TextInputDialog
|
||||
import app.revanced.manager.ui.component.haptics.HapticSwitch
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.compose.koinInject
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun BundleInformationDialog(
|
||||
src: PatchBundleSource,
|
||||
patchCount: Int,
|
||||
onDismissRequest: () -> Unit,
|
||||
onDeleteRequest: () -> Unit,
|
||||
bundle: PatchBundleSource,
|
||||
onUpdate: () -> Unit,
|
||||
) {
|
||||
val bundleRepo = koinInject<PatchBundleRepository>()
|
||||
val networkInfo = koinInject<NetworkInfo>()
|
||||
val prefs = koinInject<PreferencesManager>()
|
||||
val hasNetwork = remember { networkInfo.isConnected() }
|
||||
val composableScope = rememberCoroutineScope()
|
||||
var viewCurrentBundlePatches by remember { mutableStateOf(false) }
|
||||
val isLocal = src is LocalPatchBundle
|
||||
val bundleManifestAttributes = src.patchBundle?.manifestAttributes
|
||||
val (autoUpdate, endpoint) = src.asRemoteOrNull?.let { it.autoUpdate to it.endpoint }
|
||||
?: (null to null)
|
||||
|
||||
fun onAutoUpdateChange(new: Boolean) = composableScope.launch {
|
||||
with(bundleRepo) {
|
||||
src.asRemoteOrNull?.setAutoUpdate(new)
|
||||
}
|
||||
val isLocal = bundle is LocalPatchBundle
|
||||
val state by bundle.state.collectAsStateWithLifecycle()
|
||||
val props by remember(bundle) {
|
||||
bundle.propsFlow()
|
||||
}.collectAsStateWithLifecycle(null)
|
||||
val patchCount = remember(state) {
|
||||
state.patchBundleOrNull()?.patches?.size ?: 0
|
||||
}
|
||||
|
||||
if (viewCurrentBundlePatches) {
|
||||
BundlePatchesDialog(
|
||||
src = src,
|
||||
onDismissRequest = {
|
||||
viewCurrentBundlePatches = false
|
||||
}
|
||||
},
|
||||
bundle = bundle,
|
||||
)
|
||||
}
|
||||
|
||||
FullscreenDialog(
|
||||
Dialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
properties = DialogProperties(
|
||||
usePlatformDefaultWidth = false,
|
||||
dismissOnBackPress = true
|
||||
)
|
||||
) {
|
||||
val bundleName by bundle.nameState
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
BundleTopBar(
|
||||
title = src.name,
|
||||
title = stringResource(R.string.patch_bundle_field),
|
||||
onBackClick = onDismissRequest,
|
||||
backIcon = {
|
||||
Icon(
|
||||
@@ -105,7 +73,7 @@ fun BundleInformationDialog(
|
||||
)
|
||||
},
|
||||
actions = {
|
||||
if (!src.isDefault) {
|
||||
if (!bundle.isDefault) {
|
||||
IconButton(onClick = onDeleteRequest) {
|
||||
Icon(
|
||||
Icons.Outlined.DeleteOutline,
|
||||
@@ -113,7 +81,7 @@ fun BundleInformationDialog(
|
||||
)
|
||||
}
|
||||
}
|
||||
if (!isLocal && hasNetwork) {
|
||||
if (!isLocal) {
|
||||
IconButton(onClick = onUpdate) {
|
||||
Icon(
|
||||
Icons.Outlined.Update,
|
||||
@@ -125,203 +93,54 @@ fun BundleInformationDialog(
|
||||
)
|
||||
},
|
||||
) { paddingValues ->
|
||||
ColumnWithScrollbar(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(paddingValues),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Tag(Icons.Outlined.Sell, src.name)
|
||||
bundleManifestAttributes?.description?.let {
|
||||
Tag(Icons.Outlined.Description, it)
|
||||
BaseBundleDialog(
|
||||
modifier = Modifier.padding(paddingValues),
|
||||
isDefault = bundle.isDefault,
|
||||
name = bundleName,
|
||||
remoteUrl = bundle.asRemoteOrNull?.endpoint,
|
||||
patchCount = patchCount,
|
||||
version = props?.version,
|
||||
autoUpdate = props?.autoUpdate ?: false,
|
||||
onAutoUpdateChange = {
|
||||
composableScope.launch {
|
||||
bundle.asRemoteOrNull?.setAutoUpdate(it)
|
||||
}
|
||||
bundleManifestAttributes?.source?.let {
|
||||
Tag(Icons.Outlined.Commit, it)
|
||||
}
|
||||
bundleManifestAttributes?.author?.let {
|
||||
Tag(Icons.Outlined.Person, it)
|
||||
}
|
||||
bundleManifestAttributes?.contact?.let {
|
||||
Tag(Icons.AutoMirrored.Outlined.Send, it)
|
||||
}
|
||||
bundleManifestAttributes?.website?.let {
|
||||
Tag(Icons.Outlined.Language, it, isUrl = true)
|
||||
}
|
||||
bundleManifestAttributes?.license?.let {
|
||||
Tag(Icons.Outlined.Gavel, it)
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
color = MaterialTheme.colorScheme.outlineVariant
|
||||
)
|
||||
|
||||
if (autoUpdate != null) {
|
||||
BundleListItem(
|
||||
headlineText = stringResource(auto_update),
|
||||
supportingText = stringResource(auto_update_description),
|
||||
trailingContent = {
|
||||
HapticSwitch(
|
||||
checked = autoUpdate,
|
||||
onCheckedChange = ::onAutoUpdateChange
|
||||
)
|
||||
},
|
||||
modifier = Modifier.clickable {
|
||||
onAutoUpdateChange(!autoUpdate)
|
||||
},
|
||||
onPatchesClick = {
|
||||
viewCurrentBundlePatches = true
|
||||
},
|
||||
extraFields = {
|
||||
(state as? PatchBundleSource.State.Failed)?.throwable?.let {
|
||||
var showDialog by rememberSaveable {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
)
|
||||
}
|
||||
if (showDialog) ExceptionViewerDialog(
|
||||
onDismiss = { showDialog = false },
|
||||
text = remember(it) { it.stackTraceToString() }
|
||||
)
|
||||
|
||||
if (src.isDefault) {
|
||||
val useBundlePrerelease by prefs.usePatchesPrereleases.getAsState()
|
||||
|
||||
BundleListItem(
|
||||
headlineText = stringResource(R.string.patches_prereleases),
|
||||
supportingText = stringResource(R.string.patches_prereleases_description, src.name),
|
||||
trailingContent = {
|
||||
HapticSwitch(
|
||||
checked = useBundlePrerelease,
|
||||
onCheckedChange = {
|
||||
composableScope.launch {
|
||||
prefs.usePatchesPrereleases.update(
|
||||
it
|
||||
)
|
||||
onUpdate()
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
modifier = Modifier.clickable {
|
||||
composableScope.launch {
|
||||
prefs.usePatchesPrereleases.update(!useBundlePrerelease)
|
||||
onUpdate()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
endpoint?.takeUnless { src.isDefault }?.let { url ->
|
||||
var showUrlInputDialog by rememberSaveable {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
if (showUrlInputDialog) {
|
||||
TextInputDialog(
|
||||
initial = url,
|
||||
title = stringResource(patches_url),
|
||||
onDismissRequest = { showUrlInputDialog = false },
|
||||
onConfirm = {
|
||||
showUrlInputDialog = false
|
||||
TODO("Not implemented.")
|
||||
BundleListItem(
|
||||
headlineText = stringResource(R.string.bundle_error),
|
||||
supportingText = stringResource(R.string.bundle_error_description),
|
||||
trailingContent = {
|
||||
Icon(
|
||||
Icons.AutoMirrored.Outlined.ArrowRight,
|
||||
null
|
||||
)
|
||||
},
|
||||
validator = {
|
||||
if (it.isEmpty()) return@TextInputDialog false
|
||||
|
||||
isValidUrl(it)
|
||||
}
|
||||
modifier = Modifier.clickable { showDialog = true }
|
||||
)
|
||||
}
|
||||
|
||||
BundleListItem(
|
||||
modifier = Modifier.clickable(
|
||||
enabled = false,
|
||||
onClick = {
|
||||
showUrlInputDialog = true
|
||||
}
|
||||
),
|
||||
headlineText = stringResource(patches_url),
|
||||
supportingText = url.ifEmpty {
|
||||
stringResource(field_not_set)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
val patchesClickable = patchCount > 0
|
||||
BundleListItem(
|
||||
headlineText = stringResource(patches),
|
||||
supportingText = stringResource(view_patches),
|
||||
modifier = Modifier.clickable(
|
||||
enabled = patchesClickable,
|
||||
onClick = {
|
||||
viewCurrentBundlePatches = true
|
||||
}
|
||||
)
|
||||
) {
|
||||
if (patchesClickable) {
|
||||
Icon(
|
||||
Icons.AutoMirrored.Outlined.ArrowRight,
|
||||
stringResource(patches)
|
||||
if (state is PatchBundleSource.State.Missing && !isLocal) {
|
||||
BundleListItem(
|
||||
headlineText = stringResource(R.string.bundle_error),
|
||||
supportingText = stringResource(R.string.bundle_not_downloaded),
|
||||
modifier = Modifier.clickable(onClick = onUpdate)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
src.error?.let {
|
||||
var showDialog by rememberSaveable {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
if (showDialog) ExceptionViewerDialog(
|
||||
onDismiss = { showDialog = false },
|
||||
text = remember(it) { it.stackTraceToString() }
|
||||
)
|
||||
|
||||
BundleListItem(
|
||||
headlineText = stringResource(R.string.patches_error),
|
||||
supportingText = stringResource(R.string.patches_error_description),
|
||||
trailingContent = {
|
||||
Icon(
|
||||
Icons.AutoMirrored.Outlined.ArrowRight,
|
||||
null
|
||||
)
|
||||
},
|
||||
modifier = Modifier.clickable { showDialog = true }
|
||||
)
|
||||
}
|
||||
if (src.state is PatchBundleSource.State.Missing && !isLocal) {
|
||||
BundleListItem(
|
||||
headlineText = stringResource(R.string.patches_error),
|
||||
supportingText = stringResource(R.string.patches_not_downloaded),
|
||||
modifier = Modifier.clickable(onClick = onUpdate)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Tag(
|
||||
icon: ImageVector,
|
||||
text: String,
|
||||
isUrl: Boolean = false
|
||||
) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = if (isUrl) {
|
||||
Modifier
|
||||
.clickable {
|
||||
try {
|
||||
uriHandler.openUri(text)
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
}
|
||||
} else
|
||||
Modifier,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
Text(
|
||||
text,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = if (isUrl) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outline,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Delete
|
||||
import androidx.compose.material.icons.outlined.ErrorOutline
|
||||
import androidx.compose.material.icons.outlined.Warning
|
||||
import androidx.compose.material3.Icon
|
||||
@@ -24,46 +23,41 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.domain.bundles.PatchBundleSource
|
||||
import app.revanced.manager.ui.component.ConfirmDialog
|
||||
import app.revanced.manager.ui.component.haptics.HapticCheckbox
|
||||
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.nameState
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun BundleItem(
|
||||
src: PatchBundleSource,
|
||||
patchCount: Int,
|
||||
selectable: Boolean,
|
||||
isBundleSelected: Boolean,
|
||||
toggleSelection: (Boolean) -> Unit,
|
||||
onSelect: () -> Unit,
|
||||
bundle: PatchBundleSource,
|
||||
onDelete: () -> Unit,
|
||||
onUpdate: () -> Unit,
|
||||
selectable: Boolean,
|
||||
onSelect: () -> Unit,
|
||||
isBundleSelected: Boolean,
|
||||
toggleSelection: (Boolean) -> Unit,
|
||||
) {
|
||||
var viewBundleDialogPage by rememberSaveable { mutableStateOf(false) }
|
||||
var showDeleteConfirmationDialog by rememberSaveable { mutableStateOf(false) }
|
||||
val state by bundle.state.collectAsStateWithLifecycle()
|
||||
|
||||
val version by remember(bundle) {
|
||||
bundle.propsFlow().map { props -> props?.version }
|
||||
}.collectAsStateWithLifecycle(null)
|
||||
val name by bundle.nameState
|
||||
|
||||
if (viewBundleDialogPage) {
|
||||
BundleInformationDialog(
|
||||
src = src,
|
||||
patchCount = patchCount,
|
||||
onDismissRequest = { viewBundleDialogPage = false },
|
||||
onDeleteRequest = { showDeleteConfirmationDialog = true },
|
||||
onUpdate = onUpdate,
|
||||
)
|
||||
}
|
||||
|
||||
if (showDeleteConfirmationDialog) {
|
||||
ConfirmDialog(
|
||||
onDismiss = { showDeleteConfirmationDialog = false },
|
||||
onConfirm = {
|
||||
onDelete()
|
||||
onDeleteRequest = {
|
||||
viewBundleDialogPage = false
|
||||
onDelete()
|
||||
},
|
||||
title = stringResource(R.string.delete),
|
||||
description = stringResource(R.string.patches_delete_single_dialog_description, src.name),
|
||||
icon = Icons.Outlined.Delete
|
||||
bundle = bundle,
|
||||
onUpdate = onUpdate,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -84,19 +78,19 @@ fun BundleItem(
|
||||
}
|
||||
} else null,
|
||||
|
||||
headlineContent = { Text(src.name) },
|
||||
headlineContent = { Text(name) },
|
||||
supportingContent = {
|
||||
if (src.state is PatchBundleSource.State.Available) {
|
||||
state.patchBundleOrNull()?.patches?.size?.let { patchCount ->
|
||||
Text(pluralStringResource(R.plurals.patch_count, patchCount, patchCount))
|
||||
}
|
||||
},
|
||||
trailingContent = {
|
||||
Row {
|
||||
val icon = remember(src.state) {
|
||||
when (src.state) {
|
||||
is PatchBundleSource.State.Failed -> Icons.Outlined.ErrorOutline to R.string.patches_error
|
||||
is PatchBundleSource.State.Missing -> Icons.Outlined.Warning to R.string.patches_missing
|
||||
is PatchBundleSource.State.Available -> null
|
||||
val icon = remember(state) {
|
||||
when (state) {
|
||||
is PatchBundleSource.State.Failed -> Icons.Outlined.ErrorOutline to R.string.bundle_error
|
||||
is PatchBundleSource.State.Missing -> Icons.Outlined.Warning to R.string.bundle_missing
|
||||
is PatchBundleSource.State.Loaded -> null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,7 +103,7 @@ fun BundleItem(
|
||||
)
|
||||
}
|
||||
|
||||
src.version?.let { Text(text = it) }
|
||||
version?.let { Text(text = it) }
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@@ -12,7 +12,6 @@ import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -23,37 +22,36 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.domain.bundles.PatchBundleSource
|
||||
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||
import app.revanced.manager.patcher.patch.PatchInfo
|
||||
import app.revanced.manager.ui.component.ArrowButton
|
||||
import app.revanced.manager.ui.component.FullscreenDialog
|
||||
import app.revanced.manager.ui.component.LazyColumnWithScrollbar
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import org.koin.compose.koinInject
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun BundlePatchesDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
src: PatchBundleSource,
|
||||
bundle: PatchBundleSource,
|
||||
) {
|
||||
var showAllVersions by rememberSaveable { mutableStateOf(false) }
|
||||
var showOptions by rememberSaveable { mutableStateOf(false) }
|
||||
val patchBundleRepository: PatchBundleRepository = koinInject()
|
||||
val patches by remember(src.uid) {
|
||||
patchBundleRepository.bundleInfoFlow.mapNotNull { it[src.uid]?.patches }
|
||||
}.collectAsStateWithLifecycle(emptyList())
|
||||
val state by bundle.state.collectAsStateWithLifecycle()
|
||||
|
||||
FullscreenDialog(
|
||||
Dialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
properties = DialogProperties(
|
||||
usePlatformDefaultWidth = false,
|
||||
dismissOnBackPress = true
|
||||
)
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
BundleTopBar(
|
||||
title = stringResource(R.string.patches),
|
||||
title = stringResource(R.string.bundle_patches),
|
||||
onBackClick = onDismissRequest,
|
||||
backIcon = {
|
||||
Icon(
|
||||
@@ -71,14 +69,16 @@ fun BundlePatchesDialog(
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
contentPadding = PaddingValues(16.dp)
|
||||
) {
|
||||
items(patches) { patch ->
|
||||
PatchItem(
|
||||
patch,
|
||||
showAllVersions,
|
||||
onExpandVersions = { showAllVersions = !showAllVersions },
|
||||
showOptions,
|
||||
onExpandOptions = { showOptions = !showOptions }
|
||||
)
|
||||
state.patchBundleOrNull()?.let { bundle ->
|
||||
items(bundle.patches) { patch ->
|
||||
PatchItem(
|
||||
patch,
|
||||
showAllVersions,
|
||||
onExpandVersions = { showAllVersions = !showAllVersions },
|
||||
showOptions,
|
||||
onExpandOptions = { showOptions = !showOptions }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -138,10 +138,10 @@ fun PatchItem(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
PatchInfoChip(
|
||||
text = "$PACKAGE_ICON ${stringResource(R.string.patches_view_any_package)}"
|
||||
text = "$PACKAGE_ICON ${stringResource(R.string.bundle_view_patches_any_package)}"
|
||||
)
|
||||
PatchInfoChip(
|
||||
text = "$VERSION_ICON ${stringResource(R.string.patches_view_any_version)}"
|
||||
text = "$VERSION_ICON ${stringResource(R.string.bundle_view_patches_any_version)}"
|
||||
)
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -12,23 +12,23 @@ import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.domain.bundles.PatchBundleSource
|
||||
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.nameState
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun BundleSelector(sources: List<PatchBundleSource>, onFinish: (PatchBundleSource?) -> Unit) {
|
||||
LaunchedEffect(sources) {
|
||||
if (sources.size == 1) {
|
||||
onFinish(sources[0])
|
||||
fun BundleSelector(bundles: List<PatchBundleSource>, onFinish: (PatchBundleSource?) -> Unit) {
|
||||
LaunchedEffect(bundles) {
|
||||
if (bundles.size == 1) {
|
||||
onFinish(bundles[0])
|
||||
}
|
||||
}
|
||||
|
||||
if (sources.size < 2) {
|
||||
if (bundles.size < 2) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -47,12 +47,13 @@ fun BundleSelector(sources: List<PatchBundleSource>, onFinish: (PatchBundleSourc
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.select),
|
||||
text = "Select bundle",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
sources.forEach {
|
||||
bundles.forEach {
|
||||
val name by it.nameState
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
@@ -64,7 +65,7 @@ fun BundleSelector(sources: List<PatchBundleSource>, onFinish: (PatchBundleSourc
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
"${it.name} ${it.version}",
|
||||
name,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
|
||||
@@ -23,14 +23,10 @@ import app.revanced.manager.ui.component.AlertDialogExtended
|
||||
import app.revanced.manager.ui.component.TextHorizontalPadding
|
||||
import app.revanced.manager.ui.component.haptics.HapticCheckbox
|
||||
import app.revanced.manager.ui.component.haptics.HapticRadioButton
|
||||
import app.revanced.manager.ui.model.BundleType
|
||||
import app.revanced.manager.util.BIN_MIMETYPE
|
||||
import app.revanced.manager.util.transparentListItemColors
|
||||
|
||||
private enum class BundleType {
|
||||
Local,
|
||||
Remote
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ImportPatchBundleDialog(
|
||||
onDismiss: () -> Unit,
|
||||
@@ -41,7 +37,7 @@ fun ImportPatchBundleDialog(
|
||||
var bundleType by rememberSaveable { mutableStateOf(BundleType.Remote) }
|
||||
var patchBundle by rememberSaveable { mutableStateOf<Uri?>(null) }
|
||||
var remoteUrl by rememberSaveable { mutableStateOf("") }
|
||||
var autoUpdate by rememberSaveable { mutableStateOf(true) }
|
||||
var autoUpdate by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
val patchActivityLauncher =
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri ->
|
||||
@@ -81,7 +77,7 @@ fun ImportPatchBundleDialog(
|
||||
AlertDialogExtended(
|
||||
onDismissRequest = onDismiss,
|
||||
title = {
|
||||
Text(stringResource(if (currentStep == 0) R.string.select else R.string.add_patches))
|
||||
Text(stringResource(if (currentStep == 0) R.string.select else R.string.add_patch_bundle))
|
||||
},
|
||||
text = {
|
||||
steps[currentStep]()
|
||||
@@ -121,7 +117,7 @@ fun ImportPatchBundleDialog(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SelectBundleTypeStep(
|
||||
fun SelectBundleTypeStep(
|
||||
bundleType: BundleType,
|
||||
onBundleTypeSelected: (BundleType) -> Unit
|
||||
) {
|
||||
@@ -130,7 +126,7 @@ private fun SelectBundleTypeStep(
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.padding(horizontal = 24.dp),
|
||||
text = stringResource(R.string.select_patches_type_dialog_description)
|
||||
text = stringResource(R.string.select_bundle_type_dialog_description)
|
||||
)
|
||||
Column {
|
||||
ListItem(
|
||||
@@ -140,7 +136,7 @@ private fun SelectBundleTypeStep(
|
||||
),
|
||||
headlineContent = { Text(stringResource(R.string.enter_url)) },
|
||||
overlineContent = { Text(stringResource(R.string.recommended)) },
|
||||
supportingContent = { Text(stringResource(R.string.remote_patches_description)) },
|
||||
supportingContent = { Text(stringResource(R.string.remote_bundle_description)) },
|
||||
leadingContent = {
|
||||
HapticRadioButton(
|
||||
selected = bundleType == BundleType.Remote,
|
||||
@@ -156,7 +152,7 @@ private fun SelectBundleTypeStep(
|
||||
onClick = { onBundleTypeSelected(BundleType.Local) }
|
||||
),
|
||||
headlineContent = { Text(stringResource(R.string.select_from_storage)) },
|
||||
supportingContent = { Text(stringResource(R.string.local_patches_description)) },
|
||||
supportingContent = { Text(stringResource(R.string.local_bundle_description)) },
|
||||
overlineContent = { },
|
||||
leadingContent = {
|
||||
HapticRadioButton(
|
||||
@@ -172,7 +168,7 @@ private fun SelectBundleTypeStep(
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun ImportBundleStep(
|
||||
fun ImportBundleStep(
|
||||
bundleType: BundleType,
|
||||
patchBundle: Uri?,
|
||||
remoteUrl: String,
|
||||
@@ -189,7 +185,7 @@ private fun ImportBundleStep(
|
||||
) {
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(stringResource(R.string.patches))
|
||||
Text(stringResource(R.string.patch_bundle_field))
|
||||
},
|
||||
supportingContent = { Text(stringResource(if (patchBundle != null) R.string.file_field_set else R.string.file_field_not_set)) },
|
||||
trailingContent = {
|
||||
@@ -210,11 +206,11 @@ private fun ImportBundleStep(
|
||||
OutlinedTextField(
|
||||
value = remoteUrl,
|
||||
onValueChange = onRemoteUrlChange,
|
||||
label = { Text(stringResource(R.string.patches_url)) }
|
||||
label = { Text(stringResource(R.string.bundle_url)) }
|
||||
)
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 5.dp)
|
||||
modifier = Modifier.padding(horizontal = 8.dp)
|
||||
) {
|
||||
ListItem(
|
||||
modifier = Modifier.clickable(
|
||||
|
||||
@@ -9,7 +9,6 @@ import androidx.compose.material3.SwitchDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
|
||||
@Composable
|
||||
fun HapticSwitch(
|
||||
@@ -21,19 +20,16 @@ fun HapticSwitch(
|
||||
colors: SwitchColors = SwitchDefaults.colors(),
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
) {
|
||||
val view = LocalView.current
|
||||
Switch(
|
||||
checked = checked,
|
||||
onCheckedChange = { newChecked ->
|
||||
val useNewConstants = Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE
|
||||
val hapticFeedbackType = when {
|
||||
when {
|
||||
newChecked && useNewConstants -> HapticFeedbackConstants.TOGGLE_ON
|
||||
newChecked -> HapticFeedbackConstants.VIRTUAL_KEY
|
||||
!newChecked && useNewConstants -> HapticFeedbackConstants.TOGGLE_OFF
|
||||
!newChecked -> HapticFeedbackConstants.CLOCK_TICK
|
||||
else -> {HapticFeedbackConstants.VIRTUAL_KEY}
|
||||
}
|
||||
view.performHapticFeedback(hapticFeedbackType)
|
||||
onCheckedChange(newChecked)
|
||||
},
|
||||
modifier = modifier,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package app.revanced.manager.ui.component.patcher
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Cancel
|
||||
import androidx.compose.material.icons.filled.CheckCircle
|
||||
@@ -20,7 +21,6 @@ import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -39,9 +39,11 @@ import androidx.compose.ui.unit.dp
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.ui.component.ArrowButton
|
||||
import app.revanced.manager.ui.component.LoadingIndicator
|
||||
import app.revanced.manager.ui.model.ProgressKey
|
||||
import app.revanced.manager.ui.model.State
|
||||
import app.revanced.manager.ui.model.StepCategory
|
||||
import app.revanced.manager.ui.model.Step
|
||||
import app.revanced.manager.ui.model.StepCategory
|
||||
import app.revanced.manager.ui.model.StepProgressProvider
|
||||
import java.util.Locale
|
||||
import kotlin.math.floor
|
||||
|
||||
@@ -50,10 +52,21 @@ import kotlin.math.floor
|
||||
fun Steps(
|
||||
category: StepCategory,
|
||||
steps: List<Step>,
|
||||
isExpanded: Boolean = false,
|
||||
onExpand: () -> Unit,
|
||||
onClick: () -> Unit
|
||||
stepCount: Pair<Int, Int>? = null,
|
||||
stepProgressProvider: StepProgressProvider
|
||||
) {
|
||||
var expanded by rememberSaveable { mutableStateOf(true) }
|
||||
|
||||
val categoryColor by animateColorAsState(
|
||||
if (expanded) MaterialTheme.colorScheme.surfaceContainerHigh else Color.Transparent,
|
||||
label = "category"
|
||||
)
|
||||
|
||||
val cardColor by animateColorAsState(
|
||||
if (expanded) MaterialTheme.colorScheme.surfaceContainer else Color.Transparent,
|
||||
label = "card"
|
||||
)
|
||||
|
||||
val state = remember(steps) {
|
||||
when {
|
||||
steps.all { it.state == State.COMPLETED } -> State.COMPLETED
|
||||
@@ -63,69 +76,62 @@ fun Steps(
|
||||
}
|
||||
}
|
||||
|
||||
val filteredSteps = remember(steps) {
|
||||
val failedCount = steps.count { it.state == State.FAILED }
|
||||
|
||||
steps.filter { step ->
|
||||
// Show hidden steps if it's the only failed step.
|
||||
!step.hide || (step.state == State.FAILED && failedCount == 1)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(state) {
|
||||
if (state == State.RUNNING || state == State.FAILED)
|
||||
onExpand()
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.clip(MaterialTheme.shapes.large)
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.fillMaxWidth()
|
||||
.background(MaterialTheme.colorScheme.surfaceContainerLow)
|
||||
.background(cardColor)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(14.dp),
|
||||
modifier = Modifier
|
||||
.clickable(true, onClick = onClick)
|
||||
.fillMaxWidth()
|
||||
.padding(20.dp)
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.clickable { expanded = !expanded }
|
||||
.background(categoryColor)
|
||||
) {
|
||||
StepIcon(state = state, size = 24.dp)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
modifier = Modifier.padding(16.dp)
|
||||
) {
|
||||
StepIcon(state = state, size = 24.dp)
|
||||
|
||||
Text(stringResource(category.displayName))
|
||||
Text(stringResource(category.displayName))
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
Text(
|
||||
text = "${filteredSteps.count { it.state == State.COMPLETED }}/${filteredSteps.size}",
|
||||
style = MaterialTheme.typography.labelSmall
|
||||
)
|
||||
val stepProgress = remember(stepCount, steps) {
|
||||
stepCount?.let { (current, total) -> "$current/$total" }
|
||||
?: "${steps.count { it.state == State.COMPLETED }}/${steps.size}"
|
||||
}
|
||||
|
||||
ArrowButton(modifier = Modifier.size(24.dp), expanded = isExpanded, onClick = null)
|
||||
Text(
|
||||
text = stepProgress,
|
||||
style = MaterialTheme.typography.labelSmall
|
||||
)
|
||||
|
||||
ArrowButton(modifier = Modifier.size(24.dp), expanded = expanded, onClick = null)
|
||||
}
|
||||
}
|
||||
|
||||
AnimatedVisibility(visible = isExpanded) {
|
||||
AnimatedVisibility(visible = expanded) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.background(MaterialTheme.colorScheme.background.copy(0.6f))
|
||||
.fillMaxWidth()
|
||||
.padding(top = 10.dp)
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
filteredSteps.forEachIndexed { index, step ->
|
||||
val (progress, progressText) = step.progress?.let { (current, total) ->
|
||||
if (total != null) current.toFloat() / total.toFloat() to "${current.megaBytes}/${total.megaBytes} MB"
|
||||
else null to "${current.megaBytes} MB"
|
||||
steps.forEach { step ->
|
||||
val (progress, progressText) = when (step.progressKey) {
|
||||
null -> null
|
||||
ProgressKey.DOWNLOAD -> stepProgressProvider.downloadProgress?.let { (downloaded, total) ->
|
||||
if (total != null) downloaded.toFloat() / total.toFloat() to "${downloaded.megaBytes}/${total.megaBytes} MB"
|
||||
else null to "${downloaded.megaBytes} MB"
|
||||
}
|
||||
} ?: (null to null)
|
||||
|
||||
SubStep(
|
||||
name = step.title,
|
||||
name = step.name,
|
||||
state = step.state,
|
||||
message = step.message,
|
||||
progress = progress,
|
||||
progressText = progressText,
|
||||
isFirst = index == 0,
|
||||
isLast = index == filteredSteps.lastIndex,
|
||||
progressText = progressText
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -139,9 +145,7 @@ fun SubStep(
|
||||
state: State,
|
||||
message: String? = null,
|
||||
progress: Float? = null,
|
||||
progressText: String? = null,
|
||||
isFirst: Boolean = false,
|
||||
isLast: Boolean = false,
|
||||
progressText: String? = null
|
||||
) {
|
||||
var messageExpanded by rememberSaveable { mutableStateOf(true) }
|
||||
|
||||
@@ -152,22 +156,22 @@ fun SubStep(
|
||||
clickable { messageExpanded = !messageExpanded }
|
||||
else this
|
||||
}
|
||||
.padding(top = if (isFirst) 10.dp else 8.dp, bottom = if (isLast) 20.dp else 8.dp)
|
||||
.padding(horizontal = 20.dp)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
) {
|
||||
StepIcon(
|
||||
size = 18.dp,
|
||||
state = state,
|
||||
progress = progress,
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier.size(24.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
StepIcon(state, progress, size = 20.dp)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = name,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.weight(1f, true),
|
||||
@@ -197,7 +201,7 @@ fun SubStep(
|
||||
text = message.orEmpty(),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
modifier = Modifier.padding(horizontal = 36.dp, vertical = 8.dp)
|
||||
modifier = Modifier.padding(horizontal = 52.dp, vertical = 8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -207,44 +211,40 @@ fun SubStep(
|
||||
fun StepIcon(state: State, progress: Float? = null, size: Dp) {
|
||||
val strokeWidth = Dp(floor(size.value / 10) + 1)
|
||||
|
||||
Crossfade(targetState = state, label = "State CrossFade") { state ->
|
||||
when (state) {
|
||||
State.COMPLETED -> Icon(
|
||||
Icons.Filled.CheckCircle,
|
||||
contentDescription = stringResource(R.string.step_completed),
|
||||
tint = Color(0xFF59B463),
|
||||
modifier = Modifier.size(size)
|
||||
when (state) {
|
||||
State.COMPLETED -> Icon(
|
||||
Icons.Filled.CheckCircle,
|
||||
contentDescription = stringResource(R.string.step_completed),
|
||||
tint = MaterialTheme.colorScheme.surfaceTint,
|
||||
modifier = Modifier.size(size)
|
||||
)
|
||||
|
||||
State.FAILED -> Icon(
|
||||
Icons.Filled.Cancel,
|
||||
contentDescription = stringResource(R.string.step_failed),
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.size(size)
|
||||
)
|
||||
|
||||
State.WAITING -> Icon(
|
||||
Icons.Outlined.Circle,
|
||||
contentDescription = stringResource(R.string.step_waiting),
|
||||
tint = MaterialTheme.colorScheme.surfaceVariant,
|
||||
modifier = Modifier.size(size)
|
||||
)
|
||||
|
||||
State.RUNNING ->
|
||||
LoadingIndicator(
|
||||
modifier = stringResource(R.string.step_running).let { description ->
|
||||
Modifier
|
||||
.size(size)
|
||||
.semantics {
|
||||
contentDescription = description
|
||||
}
|
||||
},
|
||||
progress = { progress },
|
||||
strokeWidth = strokeWidth
|
||||
)
|
||||
|
||||
State.FAILED -> Icon(
|
||||
Icons.Filled.Cancel,
|
||||
contentDescription = stringResource(R.string.step_failed),
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.size(size)
|
||||
)
|
||||
|
||||
State.WAITING -> Icon(
|
||||
Icons.Outlined.Circle,
|
||||
contentDescription = stringResource(R.string.step_waiting),
|
||||
tint = MaterialTheme.colorScheme.onSurface.copy(.2f),
|
||||
modifier = Modifier.size(size)
|
||||
)
|
||||
|
||||
State.RUNNING -> {
|
||||
LoadingIndicator(
|
||||
modifier = stringResource(R.string.step_running).let { description ->
|
||||
Modifier
|
||||
.size(size)
|
||||
.semantics {
|
||||
contentDescription = description
|
||||
}
|
||||
},
|
||||
|
||||
progress = { progress },
|
||||
strokeWidth = strokeWidth
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,25 +21,8 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.DragHandle
|
||||
import androidx.compose.material.icons.outlined.Add
|
||||
import androidx.compose.material.icons.outlined.Delete
|
||||
import androidx.compose.material.icons.outlined.Edit
|
||||
import androidx.compose.material.icons.outlined.Folder
|
||||
import androidx.compose.material.icons.outlined.MoreVert
|
||||
import androidx.compose.material.icons.outlined.Restore
|
||||
import androidx.compose.material.icons.outlined.SelectAll
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisallowComposableCalls
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
@@ -55,15 +38,11 @@ import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.data.platform.Filesystem
|
||||
import app.revanced.manager.patcher.patch.Option
|
||||
import app.revanced.manager.ui.component.AlertDialogExtended
|
||||
import app.revanced.manager.ui.component.AppTopBar
|
||||
import app.revanced.manager.ui.component.FloatInputDialog
|
||||
import app.revanced.manager.ui.component.FullscreenDialog
|
||||
import app.revanced.manager.ui.component.IntInputDialog
|
||||
import app.revanced.manager.ui.component.LongInputDialog
|
||||
import app.revanced.manager.ui.component.*
|
||||
import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
|
||||
import app.revanced.manager.ui.component.haptics.HapticRadioButton
|
||||
import app.revanced.manager.ui.component.haptics.HapticSwitch
|
||||
@@ -78,38 +57,26 @@ import org.koin.compose.koinInject
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.get
|
||||
import sh.calvin.reorderable.ReorderableItem
|
||||
import sh.calvin.reorderable.rememberReorderableLazyListState
|
||||
import sh.calvin.reorderable.rememberReorderableLazyColumnState
|
||||
import java.io.Serializable
|
||||
import kotlin.random.Random
|
||||
import kotlin.reflect.typeOf
|
||||
import androidx.compose.ui.window.Dialog as ComposeDialog
|
||||
|
||||
private class OptionEditorScope<T : Any>(
|
||||
private val editor: OptionEditor<T>,
|
||||
val option: Option<T>,
|
||||
val openDialog: () -> Unit,
|
||||
val dismissDialog: () -> Unit,
|
||||
val selectionWarningEnabled: Boolean,
|
||||
val showSelectionWarning: () -> Unit,
|
||||
val value: T?,
|
||||
val setValue: (T?) -> Unit
|
||||
val setValue: (T?) -> Unit,
|
||||
) {
|
||||
fun submitDialog(value: T?) {
|
||||
setValue(value)
|
||||
dismissDialog()
|
||||
}
|
||||
|
||||
fun checkSafeguard(block: () -> Unit) {
|
||||
if (!option.required && selectionWarningEnabled)
|
||||
showSelectionWarning()
|
||||
else
|
||||
block()
|
||||
}
|
||||
|
||||
fun clickAction() {
|
||||
checkSafeguard {
|
||||
editor.clickAction(this)
|
||||
}
|
||||
}
|
||||
fun clickAction() = editor.clickAction(this)
|
||||
|
||||
@Composable
|
||||
fun ListItemTrailingContent() = editor.ListItemTrailingContent(this)
|
||||
@@ -123,7 +90,7 @@ private interface OptionEditor<T : Any> {
|
||||
|
||||
@Composable
|
||||
fun ListItemTrailingContent(scope: OptionEditorScope<T>) {
|
||||
IconButton(onClick = { scope.checkSafeguard { clickAction(scope) } }) {
|
||||
IconButton(onClick = { clickAction(scope) }) {
|
||||
Icon(Icons.Outlined.Edit, stringResource(R.string.edit))
|
||||
}
|
||||
}
|
||||
@@ -151,14 +118,11 @@ private inline fun <T : Any> WithOptionEditor(
|
||||
option: Option<T>,
|
||||
value: T?,
|
||||
noinline setValue: (T?) -> Unit,
|
||||
selectionWarningEnabled: Boolean,
|
||||
crossinline onDismissDialog: @DisallowComposableCalls () -> Unit = {},
|
||||
block: OptionEditorScope<T>.() -> Unit
|
||||
) {
|
||||
var showDialog by rememberSaveable { mutableStateOf(false) }
|
||||
var showSelectionWarningDialog by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
val scope = remember(editor, option, value, setValue, selectionWarningEnabled) {
|
||||
val scope = remember(editor, option, value, setValue) {
|
||||
OptionEditorScope(
|
||||
editor,
|
||||
option,
|
||||
@@ -167,18 +131,11 @@ private inline fun <T : Any> WithOptionEditor(
|
||||
showDialog = false
|
||||
onDismissDialog()
|
||||
},
|
||||
selectionWarningEnabled,
|
||||
showSelectionWarning = { showSelectionWarningDialog = true },
|
||||
value,
|
||||
setValue
|
||||
)
|
||||
}
|
||||
|
||||
if (showSelectionWarningDialog)
|
||||
SelectionWarningDialog(
|
||||
onDismiss = { showSelectionWarningDialog = false }
|
||||
)
|
||||
|
||||
if (showDialog) scope.Dialog()
|
||||
|
||||
scope.block()
|
||||
@@ -189,7 +146,6 @@ fun <T : Any> OptionItem(
|
||||
option: Option<T>,
|
||||
value: T?,
|
||||
setValue: (T?) -> Unit,
|
||||
selectionWarningEnabled: Boolean
|
||||
) {
|
||||
val editor = remember(option.type, option.presets) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
@@ -202,7 +158,7 @@ fun <T : Any> OptionItem(
|
||||
else baseOptionEditor
|
||||
}
|
||||
|
||||
WithOptionEditor(editor, option, value, setValue, selectionWarningEnabled) {
|
||||
WithOptionEditor(editor, option, value, setValue) {
|
||||
ListItem(
|
||||
modifier = Modifier.clickable(onClick = ::clickAction),
|
||||
headlineContent = { Text(option.title) },
|
||||
@@ -321,7 +277,7 @@ private object StringOptionEditor : OptionEditor<String> {
|
||||
|
||||
private abstract class NumberOptionEditor<T : Number> : OptionEditor<T> {
|
||||
@Composable
|
||||
abstract fun NumberDialog(
|
||||
protected abstract fun NumberDialog(
|
||||
title: String,
|
||||
current: T?,
|
||||
validator: (T?) -> Boolean,
|
||||
@@ -375,14 +331,7 @@ private object BooleanOptionEditor : OptionEditor<Boolean> {
|
||||
|
||||
@Composable
|
||||
override fun ListItemTrailingContent(scope: OptionEditorScope<Boolean>) {
|
||||
HapticSwitch(
|
||||
checked = scope.current,
|
||||
onCheckedChange = { value ->
|
||||
scope.checkSafeguard {
|
||||
scope.setValue(value)
|
||||
}
|
||||
}
|
||||
)
|
||||
HapticSwitch(checked = scope.current, onCheckedChange = scope.setValue)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -421,7 +370,6 @@ private class PresetOptionEditor<T : Any>(private val innerEditor: OptionEditor<
|
||||
scope.option,
|
||||
scope.value,
|
||||
scope.setValue,
|
||||
scope.selectionWarningEnabled,
|
||||
onDismissDialog = scope.dismissDialog
|
||||
) inner@{
|
||||
var hidePresetsDialog by rememberSaveable {
|
||||
@@ -528,8 +476,7 @@ private class ListOptionEditor<T : Serializable>(private val elementEditor: Opti
|
||||
|
||||
val lazyListState = rememberLazyListState()
|
||||
val reorderableLazyColumnState =
|
||||
// Update the list
|
||||
rememberReorderableLazyListState(lazyListState) { from, to ->
|
||||
rememberReorderableLazyColumnState(lazyListState) { from, to ->
|
||||
// Update the list
|
||||
items.add(to.index, items.removeAt(from.index))
|
||||
}
|
||||
@@ -556,8 +503,12 @@ private class ListOptionEditor<T : Serializable>(private val elementEditor: Opti
|
||||
scope.submitDialog(items.mapNotNull { it.value })
|
||||
}
|
||||
|
||||
FullscreenDialog(
|
||||
ComposeDialog(
|
||||
onDismissRequest = back,
|
||||
properties = DialogProperties(
|
||||
usePlatformDefaultWidth = false,
|
||||
dismissOnBackPress = true
|
||||
),
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
@@ -643,8 +594,7 @@ private class ListOptionEditor<T : Serializable>(private val elementEditor: Opti
|
||||
elementEditor,
|
||||
elementOption,
|
||||
value = item.value,
|
||||
setValue = { items[index] = item.copy(value = it) },
|
||||
selectionWarningEnabled = scope.selectionWarningEnabled
|
||||
setValue = { items[index] = item.copy(value = it) }
|
||||
) {
|
||||
ListItem(
|
||||
modifier = Modifier.combinedClickable(
|
||||
@@ -652,10 +602,8 @@ private class ListOptionEditor<T : Serializable>(private val elementEditor: Opti
|
||||
interactionSource = interactionSource,
|
||||
onLongClickLabel = stringResource(R.string.select),
|
||||
onLongClick = {
|
||||
if (!deleteMode) {
|
||||
deletionTargets.add(item.key)
|
||||
deleteMode = true
|
||||
}
|
||||
deletionTargets.add(item.key)
|
||||
deleteMode = true
|
||||
},
|
||||
onClick = {
|
||||
if (!deleteMode) {
|
||||
|
||||
@@ -23,9 +23,10 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.ui.component.AppTopBar
|
||||
import app.revanced.manager.ui.component.FullscreenDialog
|
||||
import app.revanced.manager.ui.component.GroupHeader
|
||||
import app.revanced.manager.ui.component.LazyColumnWithScrollbar
|
||||
import app.revanced.manager.util.saver.PathSaver
|
||||
@@ -47,8 +48,12 @@ fun PathSelectorDialog(root: Path, onSelect: (Path?) -> Unit) {
|
||||
currentDirectory.listDirectoryEntries().filter(Path::isReadable).partition(Path::isDirectory)
|
||||
}
|
||||
|
||||
FullscreenDialog(
|
||||
Dialog(
|
||||
onDismissRequest = { onSelect(null) },
|
||||
properties = DialogProperties(
|
||||
usePlatformDefaultWidth = false,
|
||||
dismissOnBackPress = true
|
||||
)
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
package app.revanced.manager.ui.component.patches
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.ui.component.SafeguardDialog
|
||||
|
||||
@Composable
|
||||
fun SelectionWarningDialog(
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
SafeguardDialog(
|
||||
onDismiss = onDismiss,
|
||||
title = R.string.warning,
|
||||
body = stringResource(R.string.selection_warning_description),
|
||||
)
|
||||
}
|
||||
@@ -7,7 +7,10 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.NewReleases
|
||||
import androidx.compose.material.icons.outlined.CalendarToday
|
||||
import androidx.compose.material.icons.outlined.Campaign
|
||||
import androidx.compose.material.icons.outlined.FileDownload
|
||||
import androidx.compose.material.icons.outlined.Sell
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
@@ -34,18 +37,28 @@ fun Changelog(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.NewReleases,
|
||||
imageVector = Icons.Outlined.Campaign,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier
|
||||
.size(32.dp)
|
||||
)
|
||||
Text(
|
||||
"${version.removePrefix("v")} ($publishDate)",
|
||||
version.removePrefix("v"),
|
||||
style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight(800)),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Tag(
|
||||
Icons.Outlined.CalendarToday,
|
||||
publishDate
|
||||
)
|
||||
}
|
||||
}
|
||||
Markdown(
|
||||
markdown,
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package app.revanced.manager.ui.component.settings
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.WarningAmber
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -11,9 +9,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.domain.manager.base.Preference
|
||||
import app.revanced.manager.ui.component.ConfirmDialog
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -32,15 +28,13 @@ fun SafeguardBooleanItem(
|
||||
}
|
||||
|
||||
if (showSafeguardWarning) {
|
||||
ConfirmDialog(
|
||||
SafeguardConfirmationDialog(
|
||||
onDismiss = { showSafeguardWarning = false },
|
||||
onConfirm = {
|
||||
coroutineScope.launch { preference.update(!value) }
|
||||
showSafeguardWarning = false
|
||||
},
|
||||
title = stringResource(id = R.string.warning),
|
||||
description = stringResource(confirmationText),
|
||||
icon = Icons.Outlined.WarningAmber
|
||||
body = stringResource(confirmationText)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package app.revanced.manager.ui.component.settings
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.WarningAmber
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import app.revanced.manager.R
|
||||
|
||||
@Composable
|
||||
fun SafeguardConfirmationDialog(
|
||||
onDismiss: () -> Unit,
|
||||
onConfirm: () -> Unit,
|
||||
body: String,
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
confirmButton = {
|
||||
TextButton(onClick = onConfirm) {
|
||||
Text(stringResource(R.string.yes))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text(stringResource(R.string.no))
|
||||
}
|
||||
},
|
||||
icon = {
|
||||
Icon(Icons.Outlined.WarningAmber, null)
|
||||
},
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(id = R.string.warning),
|
||||
style = MaterialTheme.typography.headlineSmall.copy(textAlign = TextAlign.Center)
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Text(body)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -1,17 +1,6 @@
|
||||
package app.revanced.manager.ui.component.settings
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ExpandLess
|
||||
import androidx.compose.material.icons.filled.ExpandMore
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ListItemColors
|
||||
import androidx.compose.material3.ListItemDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@@ -21,10 +10,6 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
|
||||
@Composable
|
||||
fun SettingsListItem(
|
||||
@@ -82,48 +67,4 @@ fun SettingsListItem(
|
||||
colors = colors,
|
||||
tonalElevation = tonalElevation,
|
||||
shadowElevation = shadowElevation
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun ExpandableSettingListItem(
|
||||
headlineContent: String,
|
||||
supportingContent: String,
|
||||
expandableContent: @Composable () -> Unit
|
||||
) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.animateContentSize()
|
||||
) {
|
||||
SettingsListItem(
|
||||
modifier = Modifier
|
||||
.clickable{ expanded = !expanded },
|
||||
headlineContent = headlineContent,
|
||||
supportingContent = supportingContent,
|
||||
trailingContent = {
|
||||
Icon(
|
||||
imageVector = if (expanded) Icons.Default.ExpandLess else Icons.Default.ExpandMore,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
AnimatedVisibility(visible = expanded) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 12.dp, start = 16.dp, end = 16.dp)
|
||||
.animateContentSize(
|
||||
animationSpec = tween(
|
||||
durationMillis = 500,
|
||||
easing = FastOutSlowInEasing
|
||||
)
|
||||
)
|
||||
) {
|
||||
expandableContent()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
112
app/src/main/java/app/revanced/manager/ui/model/BundleInfo.kt
Normal file
112
app/src/main/java/app/revanced/manager/ui/model/BundleInfo.kt
Normal file
@@ -0,0 +1,112 @@
|
||||
package app.revanced.manager.ui.model
|
||||
|
||||
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||
import app.revanced.manager.patcher.patch.PatchInfo
|
||||
import app.revanced.manager.util.PatchSelection
|
||||
import app.revanced.manager.util.flatMapLatestAndCombine
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
/**
|
||||
* A data class that contains patch bundle metadata for use by UI code.
|
||||
*/
|
||||
data class BundleInfo(
|
||||
val name: String,
|
||||
val uid: Int,
|
||||
val supported: List<PatchInfo>,
|
||||
val unsupported: List<PatchInfo>,
|
||||
val universal: List<PatchInfo>
|
||||
) {
|
||||
val all = sequence {
|
||||
yieldAll(supported)
|
||||
yieldAll(unsupported)
|
||||
yieldAll(universal)
|
||||
}
|
||||
|
||||
val patchCount get() = supported.size + unsupported.size + universal.size
|
||||
|
||||
fun patchSequence(allowUnsupported: Boolean) = if (allowUnsupported) {
|
||||
all
|
||||
} else {
|
||||
sequence {
|
||||
yieldAll(supported)
|
||||
yieldAll(universal)
|
||||
}
|
||||
}
|
||||
|
||||
companion object Extensions {
|
||||
inline fun Iterable<BundleInfo>.toPatchSelection(
|
||||
allowUnsupported: Boolean,
|
||||
condition: (Int, PatchInfo) -> Boolean
|
||||
): PatchSelection = this.associate { bundle ->
|
||||
val patches =
|
||||
bundle.patchSequence(allowUnsupported)
|
||||
.mapNotNullTo(mutableSetOf()) { patch ->
|
||||
patch.name.takeIf {
|
||||
condition(
|
||||
bundle.uid,
|
||||
patch
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
bundle.uid to patches
|
||||
}
|
||||
|
||||
fun PatchBundleRepository.bundleInfoFlow(packageName: String, version: String?) =
|
||||
sources.flatMapLatestAndCombine(
|
||||
combiner = { it.filterNotNull() }
|
||||
) { source ->
|
||||
// Regenerate bundle information whenever this source updates.
|
||||
source.state.map { state ->
|
||||
val bundle = state.patchBundleOrNull() ?: return@map null
|
||||
|
||||
val supported = mutableListOf<PatchInfo>()
|
||||
val unsupported = mutableListOf<PatchInfo>()
|
||||
val universal = mutableListOf<PatchInfo>()
|
||||
|
||||
bundle.patches.filter { it.compatibleWith(packageName) }.forEach {
|
||||
val targetList = when {
|
||||
it.compatiblePackages == null -> universal
|
||||
it.supports(
|
||||
packageName,
|
||||
version
|
||||
) -> supported
|
||||
|
||||
else -> unsupported
|
||||
}
|
||||
|
||||
targetList.add(it)
|
||||
}
|
||||
|
||||
BundleInfo(source.getName(), source.uid, supported, unsupported, universal)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Algorithm for determining whether all required options have been set.
|
||||
*/
|
||||
inline fun Iterable<BundleInfo>.requiredOptionsSet(
|
||||
crossinline isSelected: (BundleInfo, PatchInfo) -> Boolean,
|
||||
crossinline optionsForPatch: (BundleInfo, PatchInfo) -> Map<String, Any?>?
|
||||
) = all bundle@{ bundle ->
|
||||
bundle
|
||||
.all
|
||||
.filter { isSelected(bundle, it) }
|
||||
.all patch@{
|
||||
if (it.options.isNullOrEmpty()) return@patch true
|
||||
val opts by lazy { optionsForPatch(bundle, it).orEmpty() }
|
||||
|
||||
it.options.all option@{ option ->
|
||||
if (!option.required || option.default != null) return@option true
|
||||
|
||||
option.key in opts
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class BundleType {
|
||||
Local,
|
||||
Remote
|
||||
}
|
||||
@@ -3,7 +3,6 @@ package app.revanced.manager.ui.model
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.StringRes
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.patcher.StepId
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
enum class StepCategory(@StringRes val displayName: Int) {
|
||||
@@ -16,20 +15,19 @@ enum class State {
|
||||
WAITING, RUNNING, FAILED, COMPLETED
|
||||
}
|
||||
|
||||
enum class ProgressKey {
|
||||
DOWNLOAD
|
||||
}
|
||||
|
||||
interface StepProgressProvider {
|
||||
val downloadProgress: Pair<Long, Long?>?
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class Step(
|
||||
val id: StepId,
|
||||
val title: String,
|
||||
val name: String,
|
||||
val category: StepCategory,
|
||||
val state: State = State.WAITING,
|
||||
val message: String? = null,
|
||||
val progress: Pair<Long, Long?>? = null,
|
||||
val hide: Boolean = false,
|
||||
) : Parcelable
|
||||
|
||||
|
||||
fun Step.withState(
|
||||
state: State = this.state,
|
||||
message: String? = this.message,
|
||||
progress: Pair<Long, Long?>? = this.progress
|
||||
) = copy(state = state, message = message, progress = progress)
|
||||
val progressKey: ProgressKey? = null
|
||||
) : Parcelable
|
||||
@@ -0,0 +1,35 @@
|
||||
package app.revanced.manager.ui.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import app.revanced.manager.network.downloader.ParceledDownloaderData
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import java.io.File
|
||||
|
||||
sealed interface SelectedApp : Parcelable {
|
||||
val packageName: String
|
||||
val version: String?
|
||||
|
||||
@Parcelize
|
||||
data class Download(
|
||||
override val packageName: String,
|
||||
override val version: String?,
|
||||
val data: ParceledDownloaderData
|
||||
) : SelectedApp
|
||||
|
||||
@Parcelize
|
||||
data class Search(override val packageName: String, override val version: String?) : SelectedApp
|
||||
|
||||
@Parcelize
|
||||
data class Local(
|
||||
override val packageName: String,
|
||||
override val version: String,
|
||||
val file: File,
|
||||
val temporary: Boolean
|
||||
) : SelectedApp
|
||||
|
||||
@Parcelize
|
||||
data class Installed(
|
||||
override val packageName: String,
|
||||
override val version: String
|
||||
) : SelectedApp
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package app.revanced.manager.ui.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
sealed class SelectedSource : Parcelable {
|
||||
data object Auto : SelectedSource()
|
||||
data object Installed : SelectedSource()
|
||||
data class Downloaded(val path: String, val version: String) : SelectedSource()
|
||||
data class Local(val path: String) : SelectedSource()
|
||||
data class Plugin(val packageName: String?) : SelectedSource()
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package app.revanced.manager.ui.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
sealed class SelectedVersion : Parcelable {
|
||||
data object Auto : SelectedVersion()
|
||||
data object Any : SelectedVersion()
|
||||
data class Specific(val version: String) : SelectedVersion()
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
package app.revanced.manager.ui.model.navigation
|
||||
|
||||
import android.os.Parcelable
|
||||
import app.revanced.manager.ui.model.SelectedSource
|
||||
import app.revanced.manager.ui.model.SelectedVersion
|
||||
import app.revanced.manager.ui.model.SelectedApp
|
||||
import app.revanced.manager.util.Options
|
||||
import app.revanced.manager.util.PatchSelection
|
||||
import kotlinx.parcelize.Parcelize
|
||||
@@ -24,11 +23,10 @@ data class InstalledApplicationInfo(val packageName: String)
|
||||
data class Update(val downloadOnScreenEntry: Boolean = false)
|
||||
|
||||
@Serializable
|
||||
data object SelectedAppInfo : ComplexParameter<SelectedAppInfo.ViewModelParams> {
|
||||
data object SelectedApplicationInfo : ComplexParameter<SelectedApplicationInfo.ViewModelParams> {
|
||||
@Parcelize
|
||||
data class ViewModelParams(
|
||||
val packageName: String,
|
||||
val localPath: String? = null,
|
||||
val app: SelectedApp,
|
||||
val patches: PatchSelection? = null
|
||||
) : Parcelable
|
||||
|
||||
@@ -39,35 +37,12 @@ data object SelectedAppInfo : ComplexParameter<SelectedAppInfo.ViewModelParams>
|
||||
data object PatchesSelector : ComplexParameter<PatchesSelector.ViewModelParams> {
|
||||
@Parcelize
|
||||
data class ViewModelParams(
|
||||
val packageName: String,
|
||||
val version: String?,
|
||||
val patchSelection: PatchSelection?,
|
||||
val app: SelectedApp,
|
||||
val currentSelection: PatchSelection?,
|
||||
val options: @RawValue Options,
|
||||
) : Parcelable
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data object VersionSelector : ComplexParameter<VersionSelector.ViewModelParams> {
|
||||
@Parcelize
|
||||
data class ViewModelParams(
|
||||
val packageName: String,
|
||||
val patchSelection: PatchSelection,
|
||||
val selectedVersion: SelectedVersion,
|
||||
val localPath: String? = null,
|
||||
) : Parcelable
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data object SourceSelector : ComplexParameter<SourceSelector.ViewModelParams> {
|
||||
@Parcelize
|
||||
data class ViewModelParams(
|
||||
val packageName: String,
|
||||
val version: String?,
|
||||
val selectedSource: SelectedSource,
|
||||
val localPath: String? = null,
|
||||
) : Parcelable
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data object RequiredOptions : ComplexParameter<PatchesSelector.ViewModelParams>
|
||||
}
|
||||
@@ -76,9 +51,7 @@ data object SelectedAppInfo : ComplexParameter<SelectedAppInfo.ViewModelParams>
|
||||
data object Patcher : ComplexParameter<Patcher.ViewModelParams> {
|
||||
@Parcelize
|
||||
data class ViewModelParams(
|
||||
val packageName: String,
|
||||
val version: String?,
|
||||
val selectedSource: SelectedSource,
|
||||
val selectedApp: SelectedApp,
|
||||
val selectedPatches: PatchSelection,
|
||||
val options: @RawValue Options
|
||||
) : Parcelable
|
||||
@@ -97,9 +70,6 @@ object Settings {
|
||||
@Serializable
|
||||
data object Advanced : Destination
|
||||
|
||||
@Serializable
|
||||
data object Updates : Destination
|
||||
|
||||
@Serializable
|
||||
data object Downloads : Destination
|
||||
|
||||
@@ -119,5 +89,5 @@ object Settings {
|
||||
data object Licenses : Destination
|
||||
|
||||
@Serializable
|
||||
data object Developer : Destination
|
||||
data object DeveloperOptions : Destination
|
||||
}
|
||||
@@ -44,6 +44,7 @@ import app.revanced.manager.ui.component.LazyColumnWithScrollbar
|
||||
import app.revanced.manager.ui.component.LoadingIndicator
|
||||
import app.revanced.manager.ui.component.NonSuggestedVersionDialog
|
||||
import app.revanced.manager.ui.component.SearchView
|
||||
import app.revanced.manager.ui.model.SelectedApp
|
||||
import app.revanced.manager.ui.viewmodel.AppSelectorViewModel
|
||||
import app.revanced.manager.util.APK_MIMETYPE
|
||||
import app.revanced.manager.util.EventEffect
|
||||
@@ -53,13 +54,13 @@ import org.koin.androidx.compose.koinViewModel
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AppSelectorScreen(
|
||||
onSelect: (packageName: String) -> Unit,
|
||||
onStorageSelect: (packageName: String, path: String) -> Unit,
|
||||
onSelect: (String) -> Unit,
|
||||
onStorageSelect: (SelectedApp.Local) -> Unit,
|
||||
onBackClick: () -> Unit,
|
||||
vm: AppSelectorViewModel = koinViewModel()
|
||||
) {
|
||||
EventEffect(flow = vm.storageSelectionFlow) {
|
||||
onStorageSelect(it.first, it.second)
|
||||
onStorageSelect(it)
|
||||
}
|
||||
|
||||
val pickApkLauncher =
|
||||
@@ -82,12 +83,12 @@ fun AppSelectorScreen(
|
||||
}
|
||||
}
|
||||
|
||||
// vm.nonSuggestedVersionDialogSubject?.let {
|
||||
// NonSuggestedVersionDialog(
|
||||
// suggestedVersion = suggestedVersions[it.packageName].orEmpty(),
|
||||
// onDismiss = vm::dismissNonSuggestedVersionDialog
|
||||
// )
|
||||
// }
|
||||
vm.nonSuggestedVersionDialogSubject?.let {
|
||||
NonSuggestedVersionDialog(
|
||||
suggestedVersion = suggestedVersions[it.packageName].orEmpty(),
|
||||
onDismiss = vm::dismissNonSuggestedVersionDialog
|
||||
)
|
||||
}
|
||||
|
||||
if (search)
|
||||
SearchView(
|
||||
@@ -114,7 +115,8 @@ fun AppSelectorScreen(
|
||||
)
|
||||
},
|
||||
headlineContent = { AppLabel(app.packageInfo) },
|
||||
supportingContent = app.patches?.let {
|
||||
supportingContent = { Text(app.packageName) },
|
||||
trailingContent = app.patches?.let {
|
||||
{
|
||||
Text(
|
||||
pluralStringResource(
|
||||
@@ -212,7 +214,12 @@ fun AppSelectorScreen(
|
||||
defaultText = app.packageName
|
||||
)
|
||||
},
|
||||
supportingContent = app.patches?.let {
|
||||
supportingContent = {
|
||||
suggestedVersions[app.packageName]?.let {
|
||||
Text(stringResource(R.string.suggested_version_info, it))
|
||||
}
|
||||
},
|
||||
trailingContent = app.patches?.let {
|
||||
{
|
||||
Text(
|
||||
pluralStringResource(
|
||||
@@ -227,13 +234,7 @@ fun AppSelectorScreen(
|
||||
|
||||
}
|
||||
} else {
|
||||
item {
|
||||
Box(
|
||||
modifier = Modifier.fillParentMaxSize(), contentAlignment = Alignment.Center
|
||||
) {
|
||||
LoadingIndicator()
|
||||
}
|
||||
}
|
||||
item { LoadingIndicator() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,74 +3,52 @@ package app.revanced.manager.ui.screen
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import app.revanced.manager.domain.bundles.PatchBundleSource
|
||||
import app.revanced.manager.ui.component.LazyColumnWithScrollbar
|
||||
import app.revanced.manager.ui.component.bundle.BundleItem
|
||||
import app.revanced.manager.ui.viewmodel.BundleListViewModel
|
||||
import app.revanced.manager.util.EventEffect
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun BundleListScreen(
|
||||
viewModel: BundleListViewModel = koinViewModel(),
|
||||
eventsFlow: Flow<BundleListViewModel.Event>,
|
||||
setSelectedSourceCount: (Int) -> Unit
|
||||
onDelete: (PatchBundleSource) -> Unit,
|
||||
onUpdate: (PatchBundleSource) -> Unit,
|
||||
sources: List<PatchBundleSource>,
|
||||
selectedSources: SnapshotStateList<PatchBundleSource>,
|
||||
bundlesSelectable: Boolean,
|
||||
) {
|
||||
val patchCounts by viewModel.patchCounts.collectAsStateWithLifecycle(emptyMap())
|
||||
val sources by viewModel.sources.collectAsStateWithLifecycle(emptyList())
|
||||
|
||||
EventEffect(eventsFlow) {
|
||||
viewModel.handleEvent(it)
|
||||
}
|
||||
LaunchedEffect(viewModel.selectedSources.size) {
|
||||
setSelectedSourceCount(viewModel.selectedSources.size)
|
||||
}
|
||||
|
||||
PullToRefreshBox(
|
||||
onRefresh = viewModel::refresh,
|
||||
isRefreshing = viewModel.isRefreshing
|
||||
LazyColumnWithScrollbar(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
) {
|
||||
LazyColumnWithScrollbar(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
) {
|
||||
items(
|
||||
sources,
|
||||
key = { it.uid }
|
||||
) { source ->
|
||||
BundleItem(
|
||||
src = source,
|
||||
patchCount = patchCounts[source.uid] ?: 0,
|
||||
onDelete = {
|
||||
viewModel.delete(source)
|
||||
},
|
||||
onUpdate = {
|
||||
viewModel.update(source)
|
||||
},
|
||||
selectable = viewModel.selectedSources.size > 0,
|
||||
onSelect = {
|
||||
viewModel.selectedSources.add(source.uid)
|
||||
},
|
||||
isBundleSelected = source.uid in viewModel.selectedSources,
|
||||
toggleSelection = { bundleIsNotSelected ->
|
||||
if (bundleIsNotSelected) {
|
||||
viewModel.selectedSources.add(source.uid)
|
||||
} else {
|
||||
viewModel.selectedSources.remove(source.uid)
|
||||
}
|
||||
items(
|
||||
sources,
|
||||
key = { it.uid }
|
||||
) { source ->
|
||||
BundleItem(
|
||||
bundle = source,
|
||||
onDelete = {
|
||||
onDelete(source)
|
||||
},
|
||||
onUpdate = {
|
||||
onUpdate(source)
|
||||
},
|
||||
selectable = bundlesSelectable,
|
||||
onSelect = {
|
||||
selectedSources.add(source)
|
||||
},
|
||||
isBundleSelected = selectedSources.contains(source),
|
||||
toggleSelection = { bundleIsNotSelected ->
|
||||
if (bundleIsNotSelected) {
|
||||
selectedSources.add(source)
|
||||
} else {
|
||||
selectedSources.remove(source)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import android.net.Uri
|
||||
import android.provider.Settings
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -21,7 +20,6 @@ import androidx.compose.material.icons.filled.BatteryAlert
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.outlined.Apps
|
||||
import androidx.compose.material.icons.outlined.BugReport
|
||||
import androidx.compose.material.icons.outlined.Delete
|
||||
import androidx.compose.material.icons.outlined.DeleteOutline
|
||||
import androidx.compose.material.icons.outlined.Download
|
||||
import androidx.compose.material.icons.outlined.Refresh
|
||||
@@ -44,7 +42,6 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
@@ -57,13 +54,13 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.isDefault
|
||||
import app.revanced.manager.patcher.aapt.Aapt
|
||||
import app.revanced.manager.ui.component.AlertDialogExtended
|
||||
import app.revanced.manager.ui.component.AppTopBar
|
||||
import app.revanced.manager.ui.component.AutoUpdatesDialog
|
||||
import app.revanced.manager.ui.component.AvailableUpdateDialog
|
||||
import app.revanced.manager.ui.component.NotificationCard
|
||||
import app.revanced.manager.ui.component.ConfirmDialog
|
||||
import app.revanced.manager.ui.component.bundle.BundleTopBar
|
||||
import app.revanced.manager.ui.component.bundle.ImportPatchBundleDialog
|
||||
import app.revanced.manager.ui.component.haptics.HapticFloatingActionButton
|
||||
@@ -79,7 +76,7 @@ enum class DashboardPage(
|
||||
val icon: ImageVector
|
||||
) {
|
||||
DASHBOARD(R.string.tab_apps, Icons.Outlined.Apps),
|
||||
BUNDLES(R.string.tab_patches, Icons.Outlined.Source),
|
||||
BUNDLES(R.string.tab_bundles, Icons.Outlined.Source),
|
||||
}
|
||||
|
||||
@SuppressLint("BatteryLife")
|
||||
@@ -93,8 +90,7 @@ fun DashboardScreen(
|
||||
onDownloaderPluginClick: () -> Unit,
|
||||
onAppClick: (String) -> Unit
|
||||
) {
|
||||
var selectedSourceCount by rememberSaveable { mutableIntStateOf(0) }
|
||||
val bundlesSelectable by remember { derivedStateOf { selectedSourceCount > 0 } }
|
||||
val bundlesSelectable by remember { derivedStateOf { vm.selectedSources.size > 0 } }
|
||||
val availablePatches by vm.availablePatches.collectAsStateWithLifecycle(0)
|
||||
val showNewDownloaderPluginsNotification by vm.newDownloaderPluginsAvailable.collectAsStateWithLifecycle(
|
||||
false
|
||||
@@ -157,22 +153,11 @@ fun DashboardScreen(
|
||||
}
|
||||
)
|
||||
|
||||
var showDeleteConfirmationDialog by rememberSaveable { mutableStateOf(false) }
|
||||
if (showDeleteConfirmationDialog) {
|
||||
ConfirmDialog(
|
||||
onDismiss = { showDeleteConfirmationDialog = false },
|
||||
onConfirm = vm::deleteSources,
|
||||
title = stringResource(R.string.delete),
|
||||
description = stringResource(R.string.patches_delete_multiple_dialog_description),
|
||||
icon = Icons.Outlined.Delete
|
||||
)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
if (bundlesSelectable) {
|
||||
BundleTopBar(
|
||||
title = stringResource(R.string.patches_selected, selectedSourceCount),
|
||||
title = stringResource(R.string.bundles_selected, vm.selectedSources.size),
|
||||
onBackClick = vm::cancelSourceSelection,
|
||||
backIcon = {
|
||||
Icon(
|
||||
@@ -183,7 +168,8 @@ fun DashboardScreen(
|
||||
actions = {
|
||||
IconButton(
|
||||
onClick = {
|
||||
showDeleteConfirmationDialog = true
|
||||
vm.selectedSources.forEach { if (!it.isDefault) vm.delete(it) }
|
||||
vm.cancelSourceSelection()
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
@@ -192,7 +178,10 @@ fun DashboardScreen(
|
||||
)
|
||||
}
|
||||
IconButton(
|
||||
onClick = vm::updateSources
|
||||
onClick = {
|
||||
vm.selectedSources.forEach { vm.update(it) }
|
||||
vm.cancelSourceSelection()
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
Icons.Outlined.Refresh,
|
||||
@@ -234,7 +223,7 @@ fun DashboardScreen(
|
||||
when (pagerState.currentPage) {
|
||||
DashboardPage.DASHBOARD.ordinal -> {
|
||||
if (availablePatches < 1) {
|
||||
androidContext.toast(androidContext.getString(R.string.no_patch_found))
|
||||
androidContext.toast(androidContext.getString(R.string.patches_unavailable))
|
||||
composableScope.launch {
|
||||
pagerState.animateScrollToPage(
|
||||
DashboardPage.BUNDLES.ordinal
|
||||
@@ -275,6 +264,9 @@ fun DashboardScreen(
|
||||
}
|
||||
}
|
||||
|
||||
val showBatteryOptimizationsWarning by vm.showBatteryOptimizationsWarningFlow.collectAsStateWithLifecycle(
|
||||
false
|
||||
)
|
||||
Notifications(
|
||||
if (!Aapt.supportsDevice()) {
|
||||
{
|
||||
@@ -286,23 +278,16 @@ fun DashboardScreen(
|
||||
)
|
||||
}
|
||||
} else null,
|
||||
if (vm.showBatteryOptimizationsWarning) {
|
||||
if (showBatteryOptimizationsWarning) {
|
||||
{
|
||||
val batteryOptimizationsLauncher =
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
vm.updateBatteryOptimizationsWarning()
|
||||
}
|
||||
NotificationCard(
|
||||
isWarning = true,
|
||||
icon = Icons.Default.BatteryAlert,
|
||||
text = stringResource(R.string.battery_optimization_notification),
|
||||
onClick = {
|
||||
batteryOptimizationsLauncher.launch(
|
||||
Intent(
|
||||
Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,
|
||||
Uri.fromParts("package", androidContext.packageName, null)
|
||||
)
|
||||
)
|
||||
androidContext.startActivity(Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply {
|
||||
data = Uri.parse("package:${androidContext.packageName}")
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -344,9 +329,18 @@ fun DashboardScreen(
|
||||
}
|
||||
}
|
||||
|
||||
val sources by vm.sources.collectAsStateWithLifecycle(initialValue = emptyList())
|
||||
|
||||
BundleListScreen(
|
||||
eventsFlow = vm.bundleListEventsFlow,
|
||||
setSelectedSourceCount = { selectedSourceCount = it }
|
||||
onDelete = {
|
||||
vm.delete(it)
|
||||
},
|
||||
onUpdate = {
|
||||
vm.update(it)
|
||||
},
|
||||
sources = sources,
|
||||
selectedSources = vm.selectedSources,
|
||||
bundlesSelectable = bundlesSelectable
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,28 +7,15 @@ import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.result.contract.ActivityResultContracts.CreateDocument
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.OpenInNew
|
||||
import androidx.compose.material.icons.outlined.Cancel
|
||||
import androidx.compose.material.icons.outlined.FileDownload
|
||||
import androidx.compose.material.icons.outlined.PostAdd
|
||||
import androidx.compose.material.icons.outlined.Save
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.BottomAppBar
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
@@ -46,7 +33,6 @@ import app.revanced.manager.R
|
||||
import app.revanced.manager.data.room.apps.installed.InstallType
|
||||
import app.revanced.manager.ui.component.AppScaffold
|
||||
import app.revanced.manager.ui.component.AppTopBar
|
||||
import app.revanced.manager.ui.component.ConfirmDialog
|
||||
import app.revanced.manager.ui.component.InstallerStatusDialog
|
||||
import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
|
||||
import app.revanced.manager.ui.component.patcher.InstallPickerDialog
|
||||
@@ -55,39 +41,30 @@ import app.revanced.manager.ui.model.StepCategory
|
||||
import app.revanced.manager.ui.viewmodel.PatcherViewModel
|
||||
import app.revanced.manager.util.APK_MIMETYPE
|
||||
import app.revanced.manager.util.EventEffect
|
||||
import app.revanced.manager.util.toast
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun PatcherScreen(
|
||||
onBackClick: () -> Unit,
|
||||
viewModel: PatcherViewModel
|
||||
vm: PatcherViewModel
|
||||
) {
|
||||
fun onLeave() {
|
||||
viewModel.onBack()
|
||||
fun leaveScreen() {
|
||||
vm.onBack()
|
||||
onBackClick()
|
||||
}
|
||||
BackHandler(onBack = ::leaveScreen)
|
||||
|
||||
val context = LocalContext.current
|
||||
val exportApkLauncher =
|
||||
rememberLauncherForActivityResult(CreateDocument(APK_MIMETYPE), viewModel::export)
|
||||
rememberLauncherForActivityResult(CreateDocument(APK_MIMETYPE), vm::export)
|
||||
|
||||
val patcherSucceeded by viewModel.patcherSucceeded.observeAsState(null)
|
||||
val canInstall by remember { derivedStateOf { patcherSucceeded == true && (viewModel.installedPackageName != null || !viewModel.isInstalling) } }
|
||||
val patcherSucceeded by vm.patcherSucceeded.observeAsState(null)
|
||||
val canInstall by remember { derivedStateOf { patcherSucceeded == true && (vm.installedPackageName != null || !vm.isInstalling) } }
|
||||
var showInstallPicker by rememberSaveable { mutableStateOf(false) }
|
||||
var showDismissConfirmationDialog by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
fun onPageBack() = when {
|
||||
patcherSucceeded == null -> showDismissConfirmationDialog = true
|
||||
viewModel.isInstalling -> context.toast(context.getString(R.string.patcher_install_in_progress))
|
||||
else -> onLeave()
|
||||
}
|
||||
|
||||
BackHandler(onBack = ::onPageBack)
|
||||
|
||||
val steps by remember {
|
||||
derivedStateOf {
|
||||
viewModel.steps.groupBy { it.category }.toList()
|
||||
vm.steps.groupBy { it.category }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,44 +81,34 @@ fun PatcherScreen(
|
||||
if (showInstallPicker)
|
||||
InstallPickerDialog(
|
||||
onDismiss = { showInstallPicker = false },
|
||||
onConfirm = viewModel::install
|
||||
onConfirm = vm::install
|
||||
)
|
||||
|
||||
if (showDismissConfirmationDialog) {
|
||||
ConfirmDialog(
|
||||
onDismiss = { showDismissConfirmationDialog = false },
|
||||
onConfirm = ::onLeave,
|
||||
title = stringResource(R.string.patcher_stop_confirm_title),
|
||||
description = stringResource(R.string.patcher_stop_confirm_description),
|
||||
icon = Icons.Outlined.Cancel
|
||||
)
|
||||
}
|
||||
|
||||
viewModel.packageInstallerStatus?.let {
|
||||
InstallerStatusDialog(it, viewModel, viewModel::dismissPackageInstallerDialog)
|
||||
vm.packageInstallerStatus?.let {
|
||||
InstallerStatusDialog(it, vm, vm::dismissPackageInstallerDialog)
|
||||
}
|
||||
|
||||
val activityLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.StartActivityForResult(),
|
||||
onResult = viewModel::handleActivityResult
|
||||
onResult = vm::handleActivityResult
|
||||
)
|
||||
EventEffect(flow = viewModel.launchActivityFlow) { intent ->
|
||||
EventEffect(flow = vm.launchActivityFlow) { intent ->
|
||||
activityLauncher.launch(intent)
|
||||
}
|
||||
|
||||
viewModel.activityPromptDialog?.let { title ->
|
||||
vm.activityPromptDialog?.let { title ->
|
||||
AlertDialog(
|
||||
onDismissRequest = viewModel::rejectInteraction,
|
||||
onDismissRequest = vm::rejectInteraction,
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = viewModel::allowInteraction
|
||||
onClick = vm::allowInteraction
|
||||
) {
|
||||
Text(stringResource(R.string.continue_))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(
|
||||
onClick = viewModel::rejectInteraction
|
||||
onClick = vm::rejectInteraction
|
||||
) {
|
||||
Text(stringResource(R.string.cancel))
|
||||
}
|
||||
@@ -158,20 +125,20 @@ fun PatcherScreen(
|
||||
AppTopBar(
|
||||
title = stringResource(R.string.patcher),
|
||||
scrollBehavior = scrollBehavior,
|
||||
onBackClick = ::onPageBack
|
||||
onBackClick = ::leaveScreen
|
||||
)
|
||||
},
|
||||
bottomBar = {
|
||||
BottomAppBar(
|
||||
actions = {
|
||||
IconButton(
|
||||
onClick = { exportApkLauncher.launch("${viewModel.packageName}_${viewModel.version}_revanced_patched.apk") },
|
||||
onClick = { exportApkLauncher.launch("${vm.packageName}.apk") },
|
||||
enabled = patcherSucceeded == true
|
||||
) {
|
||||
Icon(Icons.Outlined.Save, stringResource(id = R.string.save_apk))
|
||||
}
|
||||
IconButton(
|
||||
onClick = { viewModel.exportLogs(context) },
|
||||
onClick = { vm.exportLogs(context) },
|
||||
enabled = patcherSucceeded != null
|
||||
) {
|
||||
Icon(Icons.Outlined.PostAdd, stringResource(id = R.string.save_logs))
|
||||
@@ -182,11 +149,11 @@ fun PatcherScreen(
|
||||
HapticExtendedFloatingActionButton(
|
||||
text = {
|
||||
Text(
|
||||
stringResource(if (viewModel.installedPackageName == null) R.string.install_app else R.string.open_app)
|
||||
stringResource(if (vm.installedPackageName == null) R.string.install_app else R.string.open_app)
|
||||
)
|
||||
},
|
||||
icon = {
|
||||
viewModel.installedPackageName?.let {
|
||||
vm.installedPackageName?.let {
|
||||
Icon(
|
||||
Icons.AutoMirrored.Outlined.OpenInNew,
|
||||
stringResource(R.string.open_app)
|
||||
@@ -197,10 +164,10 @@ fun PatcherScreen(
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
if (viewModel.installedPackageName == null)
|
||||
if (viewModel.isDeviceRooted()) showInstallPicker = true
|
||||
else viewModel.install(InstallType.DEFAULT)
|
||||
else viewModel.open()
|
||||
if (vm.installedPackageName == null)
|
||||
if (vm.isDeviceRooted()) showInstallPicker = true
|
||||
else vm.install(InstallType.DEFAULT)
|
||||
else vm.open()
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -213,14 +180,8 @@ fun PatcherScreen(
|
||||
.padding(paddingValues)
|
||||
.fillMaxSize()
|
||||
) {
|
||||
var expandedCategory by rememberSaveable { mutableStateOf<StepCategory?>(null) }
|
||||
|
||||
val expandCategory: (StepCategory?) -> Unit = { category ->
|
||||
expandedCategory = category
|
||||
}
|
||||
|
||||
LinearProgressIndicator(
|
||||
progress = { viewModel.progress },
|
||||
progress = { vm.progress },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
@@ -230,20 +191,17 @@ fun PatcherScreen(
|
||||
contentPadding = PaddingValues(16.dp)
|
||||
) {
|
||||
items(
|
||||
items = steps,
|
||||
items = steps.toList(),
|
||||
key = { it.first }
|
||||
) { (category, steps) ->
|
||||
Steps(
|
||||
category = category,
|
||||
steps = steps,
|
||||
isExpanded = expandedCategory == category,
|
||||
onExpand = { expandCategory(category) },
|
||||
onClick = {
|
||||
expandCategory(if (expandedCategory == category) null else category)
|
||||
}
|
||||
stepCount = if (category == StepCategory.PATCHING) vm.patchesProgress else null,
|
||||
stepProgressProvider = vm
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,14 +58,16 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.patcher.patch.Option
|
||||
import app.revanced.manager.patcher.patch.PatchInfo
|
||||
import app.revanced.manager.ui.component.AppTopBar
|
||||
import app.revanced.manager.ui.component.CheckedFilterChip
|
||||
import app.revanced.manager.ui.component.FullscreenDialog
|
||||
import app.revanced.manager.ui.component.LazyColumnWithScrollbar
|
||||
import app.revanced.manager.ui.component.SafeguardDialog
|
||||
import app.revanced.manager.ui.component.SearchBar
|
||||
@@ -73,10 +75,9 @@ import app.revanced.manager.ui.component.haptics.HapticCheckbox
|
||||
import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
|
||||
import app.revanced.manager.ui.component.haptics.HapticTab
|
||||
import app.revanced.manager.ui.component.patches.OptionItem
|
||||
import app.revanced.manager.ui.component.patches.SelectionWarningDialog
|
||||
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel
|
||||
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_INCOMPATIBLE
|
||||
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_UNIVERSAL
|
||||
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_UNSUPPORTED
|
||||
import app.revanced.manager.util.Options
|
||||
import app.revanced.manager.util.PatchSelection
|
||||
import app.revanced.manager.util.isScrollingUp
|
||||
@@ -88,9 +89,9 @@ import kotlinx.coroutines.launch
|
||||
fun PatchesSelectorScreen(
|
||||
onSave: (PatchSelection?, Options) -> Unit,
|
||||
onBackClick: () -> Unit,
|
||||
viewModel: PatchesSelectorViewModel
|
||||
vm: PatchesSelectorViewModel
|
||||
) {
|
||||
val bundles by viewModel.bundlesFlow.collectAsStateWithLifecycle(initialValue = emptyList())
|
||||
val bundles by vm.bundlesFlow.collectAsStateWithLifecycle(initialValue = emptyList())
|
||||
val pagerState = rememberPagerState(
|
||||
initialPage = 0,
|
||||
initialPageOffsetFraction = 0f
|
||||
@@ -106,15 +107,15 @@ fun PatchesSelectorScreen(
|
||||
}
|
||||
var showBottomSheet by rememberSaveable { mutableStateOf(false) }
|
||||
val showSaveButton by remember {
|
||||
derivedStateOf { viewModel.selectionIsValid(bundles) }
|
||||
derivedStateOf { vm.selectionIsValid(bundles) }
|
||||
}
|
||||
|
||||
val defaultPatchSelectionCount by viewModel.defaultSelectionCount
|
||||
val defaultPatchSelectionCount by vm.defaultSelectionCount
|
||||
.collectAsStateWithLifecycle(initialValue = 0)
|
||||
|
||||
val selectedPatchCount by remember {
|
||||
derivedStateOf {
|
||||
viewModel.customPatchSelection?.values?.sumOf { it.size } ?: defaultPatchSelectionCount
|
||||
vm.customPatchSelection?.values?.sumOf { it.size } ?: defaultPatchSelectionCount
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,14 +147,14 @@ fun PatchesSelectorScreen(
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
CheckedFilterChip(
|
||||
selected = viewModel.filter and SHOW_INCOMPATIBLE == 0,
|
||||
onClick = { viewModel.toggleFlag(SHOW_INCOMPATIBLE) },
|
||||
label = { Text(stringResource(R.string.this_version)) }
|
||||
selected = vm.filter and SHOW_UNSUPPORTED == 0,
|
||||
onClick = { vm.toggleFlag(SHOW_UNSUPPORTED) },
|
||||
label = { Text(stringResource(R.string.supported)) }
|
||||
)
|
||||
|
||||
CheckedFilterChip(
|
||||
selected = viewModel.filter and SHOW_UNIVERSAL != 0,
|
||||
onClick = { viewModel.toggleFlag(SHOW_UNIVERSAL) },
|
||||
selected = vm.filter and SHOW_UNIVERSAL != 0,
|
||||
onClick = { vm.toggleFlag(SHOW_UNIVERSAL) },
|
||||
label = { Text(stringResource(R.string.universal)) },
|
||||
)
|
||||
}
|
||||
@@ -161,46 +162,49 @@ fun PatchesSelectorScreen(
|
||||
}
|
||||
}
|
||||
|
||||
if (viewModel.compatibleVersions.isNotEmpty())
|
||||
IncompatiblePatchDialog(
|
||||
appVersion = viewModel.appVersion ?: stringResource(R.string.any_version),
|
||||
compatibleVersions = viewModel.compatibleVersions,
|
||||
onDismissRequest = viewModel::dismissDialogs
|
||||
if (vm.compatibleVersions.isNotEmpty())
|
||||
UnsupportedPatchDialog(
|
||||
appVersion = vm.appVersion ?: stringResource(R.string.any_version),
|
||||
supportedVersions = vm.compatibleVersions,
|
||||
onDismissRequest = vm::dismissDialogs
|
||||
)
|
||||
var showIncompatiblePatchesDialog by rememberSaveable {
|
||||
var showUnsupportedPatchesDialog by rememberSaveable {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
if (showIncompatiblePatchesDialog)
|
||||
IncompatiblePatchesDialog(
|
||||
appVersion = viewModel.appVersion ?: stringResource(R.string.any_version),
|
||||
onDismissRequest = { showIncompatiblePatchesDialog = false }
|
||||
if (showUnsupportedPatchesDialog)
|
||||
UnsupportedPatchesDialog(
|
||||
appVersion = vm.appVersion ?: stringResource(R.string.any_version),
|
||||
onDismissRequest = { showUnsupportedPatchesDialog = false }
|
||||
)
|
||||
|
||||
viewModel.optionsDialog?.let { (bundle, patch) ->
|
||||
vm.optionsDialog?.let { (bundle, patch) ->
|
||||
OptionsDialog(
|
||||
onDismissRequest = viewModel::dismissDialogs,
|
||||
onDismissRequest = vm::dismissDialogs,
|
||||
patch = patch,
|
||||
values = viewModel.getOptions(bundle, patch),
|
||||
reset = { viewModel.resetOptions(bundle, patch) },
|
||||
set = { key, value -> viewModel.setOption(bundle, patch, key, value) },
|
||||
selectionWarningEnabled = viewModel.selectionWarningEnabled
|
||||
values = vm.getOptions(bundle, patch),
|
||||
reset = { vm.resetOptions(bundle, patch) },
|
||||
set = { key, value -> vm.setOption(bundle, patch, key, value) }
|
||||
)
|
||||
}
|
||||
|
||||
var showSelectionWarning by rememberSaveable { mutableStateOf(false) }
|
||||
var showUniversalWarning by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
if (showSelectionWarning)
|
||||
var showSelectionWarning by rememberSaveable {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
if (showSelectionWarning) {
|
||||
SelectionWarningDialog(onDismiss = { showSelectionWarning = false })
|
||||
|
||||
if (showUniversalWarning)
|
||||
UniversalPatchWarningDialog(onDismiss = { showUniversalWarning = false })
|
||||
}
|
||||
vm.pendingUniversalPatchAction?.let {
|
||||
UniversalPatchWarningDialog(
|
||||
onCancel = vm::dismissUniversalPatchWarning,
|
||||
onConfirm = vm::confirmUniversalPatchWarning
|
||||
)
|
||||
}
|
||||
|
||||
fun LazyListScope.patchList(
|
||||
uid: Int,
|
||||
patches: List<PatchInfo>,
|
||||
visible: Boolean,
|
||||
compatible: Boolean,
|
||||
supported: Boolean,
|
||||
header: (@Composable () -> Unit)? = null
|
||||
) {
|
||||
if (patches.isNotEmpty() && visible) {
|
||||
@@ -217,27 +221,31 @@ fun PatchesSelectorScreen(
|
||||
) { patch ->
|
||||
PatchItem(
|
||||
patch = patch,
|
||||
onOptionsDialog = { viewModel.optionsDialog = uid to patch },
|
||||
selected = compatible && viewModel.isSelected(
|
||||
onOptionsDialog = {
|
||||
vm.optionsDialog = uid to patch
|
||||
},
|
||||
selected = supported && vm.isSelected(
|
||||
uid,
|
||||
patch
|
||||
),
|
||||
onToggle = {
|
||||
when {
|
||||
// Open incompatible dialog if the patch is not supported
|
||||
!compatible -> viewModel.openIncompatibleDialog(patch)
|
||||
// Open unsupported dialog if the patch is not supported
|
||||
!supported -> vm.openUnsupportedDialog(patch)
|
||||
|
||||
// Show selection warning if enabled
|
||||
viewModel.selectionWarningEnabled -> showSelectionWarning = true
|
||||
vm.selectionWarningEnabled -> showSelectionWarning = true
|
||||
|
||||
// Show universal warning if universal patch is selected and the toggle is off
|
||||
patch.compatiblePackages == null && viewModel.universalPatchWarningEnabled -> showUniversalWarning = true
|
||||
// Set pending universal patch action if the universal patch warning is enabled and there are no compatible packages
|
||||
vm.universalPatchWarningEnabled && patch.compatiblePackages == null -> {
|
||||
vm.pendingUniversalPatchAction = { vm.togglePatch(uid, patch) }
|
||||
}
|
||||
|
||||
// Toggle the patch otherwise
|
||||
else -> viewModel.togglePatch(uid, patch)
|
||||
else -> vm.togglePatch(uid, patch)
|
||||
}
|
||||
},
|
||||
compatible = compatible
|
||||
supported = supported
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -313,15 +321,15 @@ fun PatchesSelectorScreen(
|
||||
|
||||
patchList(
|
||||
uid = bundle.uid,
|
||||
patches = bundle.compatible.searched(),
|
||||
patches = bundle.supported.searched(),
|
||||
visible = true,
|
||||
compatible = true
|
||||
supported = true
|
||||
)
|
||||
patchList(
|
||||
uid = bundle.uid,
|
||||
patches = bundle.universal.searched(),
|
||||
visible = viewModel.filter and SHOW_UNIVERSAL != 0,
|
||||
compatible = true
|
||||
visible = vm.filter and SHOW_UNIVERSAL != 0,
|
||||
supported = true
|
||||
) {
|
||||
ListHeader(
|
||||
title = stringResource(R.string.universal_patches),
|
||||
@@ -330,13 +338,13 @@ fun PatchesSelectorScreen(
|
||||
|
||||
patchList(
|
||||
uid = bundle.uid,
|
||||
patches = bundle.incompatible.searched(),
|
||||
visible = viewModel.filter and SHOW_INCOMPATIBLE != 0,
|
||||
compatible = viewModel.allowIncompatiblePatches
|
||||
patches = bundle.unsupported.searched(),
|
||||
visible = vm.filter and SHOW_UNSUPPORTED != 0,
|
||||
supported = vm.allowIncompatiblePatches
|
||||
) {
|
||||
ListHeader(
|
||||
title = stringResource(R.string.incompatible_patches),
|
||||
onHelpClick = { showIncompatiblePatchesDialog = true }
|
||||
title = stringResource(R.string.unsupported_patches),
|
||||
onHelpClick = { showUnsupportedPatchesDialog = true }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -355,30 +363,22 @@ fun PatchesSelectorScreen(
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
SmallFloatingActionButton(
|
||||
onClick = viewModel::reset,
|
||||
onClick = vm::reset,
|
||||
containerColor = MaterialTheme.colorScheme.tertiaryContainer
|
||||
) {
|
||||
Icon(Icons.Outlined.Restore, stringResource(R.string.reset))
|
||||
}
|
||||
HapticExtendedFloatingActionButton(
|
||||
text = {
|
||||
Text(
|
||||
stringResource(
|
||||
R.string.save_with_count,
|
||||
selectedPatchCount
|
||||
)
|
||||
)
|
||||
},
|
||||
text = { Text(stringResource(R.string.save_with_count, selectedPatchCount)) },
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Save,
|
||||
contentDescription = stringResource(R.string.save)
|
||||
)
|
||||
},
|
||||
expanded = patchLazyListStates.getOrNull(pagerState.currentPage)?.isScrollingUp
|
||||
?: true,
|
||||
expanded = patchLazyListStates.getOrNull(pagerState.currentPage)?.isScrollingUp ?: true,
|
||||
onClick = {
|
||||
onSave(viewModel.getCustomSelection(), viewModel.getOptions())
|
||||
onSave(vm.getCustomSelection(), vm.getOptions())
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -389,7 +389,6 @@ fun PatchesSelectorScreen(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.padding(top = 16.dp)
|
||||
) {
|
||||
if (bundles.size > 1) {
|
||||
ScrollableTabRow(
|
||||
@@ -406,19 +405,7 @@ fun PatchesSelectorScreen(
|
||||
)
|
||||
}
|
||||
},
|
||||
text = {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text(
|
||||
text = bundle.name,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
Text(
|
||||
text = bundle.version.orEmpty(),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
},
|
||||
text = { Text(bundle.name) },
|
||||
selectedContentColor = MaterialTheme.colorScheme.primary,
|
||||
unselectedContentColor = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
@@ -440,15 +427,15 @@ fun PatchesSelectorScreen(
|
||||
) {
|
||||
patchList(
|
||||
uid = bundle.uid,
|
||||
patches = bundle.compatible,
|
||||
patches = bundle.supported,
|
||||
visible = true,
|
||||
compatible = true
|
||||
supported = true
|
||||
)
|
||||
patchList(
|
||||
uid = bundle.uid,
|
||||
patches = bundle.universal,
|
||||
visible = viewModel.filter and SHOW_UNIVERSAL != 0,
|
||||
compatible = true
|
||||
visible = vm.filter and SHOW_UNIVERSAL != 0,
|
||||
supported = true
|
||||
) {
|
||||
ListHeader(
|
||||
title = stringResource(R.string.universal_patches),
|
||||
@@ -456,13 +443,13 @@ fun PatchesSelectorScreen(
|
||||
}
|
||||
patchList(
|
||||
uid = bundle.uid,
|
||||
patches = bundle.incompatible,
|
||||
visible = viewModel.filter and SHOW_INCOMPATIBLE != 0,
|
||||
compatible = viewModel.allowIncompatiblePatches
|
||||
patches = bundle.unsupported,
|
||||
visible = vm.filter and SHOW_UNSUPPORTED != 0,
|
||||
supported = vm.allowIncompatiblePatches
|
||||
) {
|
||||
ListHeader(
|
||||
title = stringResource(R.string.incompatible_patches),
|
||||
onHelpClick = { showIncompatiblePatchesDialog = true }
|
||||
title = stringResource(R.string.unsupported_patches),
|
||||
onHelpClick = { showUnsupportedPatchesDialog = true }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -473,13 +460,43 @@ fun PatchesSelectorScreen(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UniversalPatchWarningDialog(
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
private fun SelectionWarningDialog(onDismiss: () -> Unit) {
|
||||
SafeguardDialog(
|
||||
onDismiss = onDismiss,
|
||||
title = R.string.warning,
|
||||
body = stringResource(R.string.universal_patch_warning_description),
|
||||
body = stringResource(R.string.selection_warning_description),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UniversalPatchWarningDialog(
|
||||
onCancel: () -> Unit,
|
||||
onConfirm: () -> Unit
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onCancel,
|
||||
confirmButton = {
|
||||
TextButton(onClick = onConfirm) {
|
||||
Text(stringResource(R.string.continue_))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onCancel) {
|
||||
Text(stringResource(R.string.cancel))
|
||||
}
|
||||
},
|
||||
icon = {
|
||||
Icon(Icons.Outlined.WarningAmber, null)
|
||||
},
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.warning),
|
||||
style = MaterialTheme.typography.headlineSmall.copy(textAlign = TextAlign.Center)
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Text(stringResource(R.string.universal_patch_warning_description))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -489,24 +506,24 @@ private fun PatchItem(
|
||||
onOptionsDialog: () -> Unit,
|
||||
selected: Boolean,
|
||||
onToggle: () -> Unit,
|
||||
compatible: Boolean = true
|
||||
supported: Boolean = true
|
||||
) = ListItem(
|
||||
modifier = Modifier
|
||||
.let { if (!compatible) it.alpha(0.5f) else it }
|
||||
.let { if (!supported) it.alpha(0.5f) else it }
|
||||
.clickable(onClick = onToggle)
|
||||
.fillMaxSize(),
|
||||
leadingContent = {
|
||||
HapticCheckbox(
|
||||
checked = selected,
|
||||
onCheckedChange = null,
|
||||
enabled = compatible
|
||||
onCheckedChange = { onToggle() },
|
||||
enabled = supported
|
||||
)
|
||||
},
|
||||
headlineContent = { Text(patch.name) },
|
||||
supportingContent = patch.description?.let { { Text(it) } },
|
||||
trailingContent = {
|
||||
if (patch.options?.isNotEmpty() == true) {
|
||||
IconButton(onClick = onOptionsDialog, enabled = compatible) {
|
||||
IconButton(onClick = onOptionsDialog, enabled = supported) {
|
||||
Icon(Icons.Outlined.Settings, null)
|
||||
}
|
||||
}
|
||||
@@ -542,7 +559,7 @@ fun ListHeader(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun IncompatiblePatchesDialog(
|
||||
private fun UnsupportedPatchesDialog(
|
||||
appVersion: String,
|
||||
onDismissRequest: () -> Unit
|
||||
) = AlertDialog(
|
||||
@@ -555,11 +572,11 @@ private fun IncompatiblePatchesDialog(
|
||||
Text(stringResource(R.string.ok))
|
||||
}
|
||||
},
|
||||
title = { Text(stringResource(R.string.incompatible_patches)) },
|
||||
title = { Text(stringResource(R.string.unsupported_patches)) },
|
||||
text = {
|
||||
Text(
|
||||
stringResource(
|
||||
R.string.incompatible_patches_dialog,
|
||||
R.string.unsupported_patches_dialog,
|
||||
appVersion
|
||||
)
|
||||
)
|
||||
@@ -567,9 +584,9 @@ private fun IncompatiblePatchesDialog(
|
||||
)
|
||||
|
||||
@Composable
|
||||
private fun IncompatiblePatchDialog(
|
||||
private fun UnsupportedPatchDialog(
|
||||
appVersion: String,
|
||||
compatibleVersions: List<String>,
|
||||
supportedVersions: List<String>,
|
||||
onDismissRequest: () -> Unit
|
||||
) = AlertDialog(
|
||||
icon = {
|
||||
@@ -581,13 +598,13 @@ private fun IncompatiblePatchDialog(
|
||||
Text(stringResource(R.string.ok))
|
||||
}
|
||||
},
|
||||
title = { Text(stringResource(R.string.incompatible_patch)) },
|
||||
title = { Text(stringResource(R.string.unsupported_patch)) },
|
||||
text = {
|
||||
Text(
|
||||
stringResource(
|
||||
R.string.app_version_not_compatible,
|
||||
R.string.app_not_supported,
|
||||
appVersion,
|
||||
compatibleVersions.joinToString(", ")
|
||||
supportedVersions.joinToString(", ")
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -601,8 +618,13 @@ private fun OptionsDialog(
|
||||
reset: () -> Unit,
|
||||
set: (String, Any?) -> Unit,
|
||||
onDismissRequest: () -> Unit,
|
||||
selectionWarningEnabled: Boolean
|
||||
) = FullscreenDialog(onDismissRequest = onDismissRequest) {
|
||||
) = Dialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
properties = DialogProperties(
|
||||
usePlatformDefaultWidth = false,
|
||||
dismissOnBackPress = true
|
||||
)
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
AppTopBar(
|
||||
@@ -632,8 +654,7 @@ private fun OptionsDialog(
|
||||
value = value,
|
||||
setValue = {
|
||||
set(key, it)
|
||||
},
|
||||
selectionWarningEnabled = selectionWarningEnabled
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,12 +30,12 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.patcher.patch.Option
|
||||
import app.revanced.manager.patcher.patch.PatchBundleInfo.Extensions.requiredOptionsSet
|
||||
import app.revanced.manager.ui.component.AppTopBar
|
||||
import app.revanced.manager.ui.component.LazyColumnWithScrollbar
|
||||
import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
|
||||
import app.revanced.manager.ui.component.haptics.HapticTab
|
||||
import app.revanced.manager.ui.component.patches.OptionItem
|
||||
import app.revanced.manager.ui.model.BundleInfo.Extensions.requiredOptionsSet
|
||||
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel
|
||||
import app.revanced.manager.util.Options
|
||||
import app.revanced.manager.util.PatchSelection
|
||||
@@ -62,7 +62,6 @@ fun RequiredOptionsScreen(
|
||||
val showContinueButton by remember {
|
||||
derivedStateOf {
|
||||
bundles.requiredOptionsSet(
|
||||
allowIncompatible = vm.allowIncompatiblePatches,
|
||||
isSelected = { bundle, patch -> vm.isSelected(bundle.uid, patch) },
|
||||
optionsForPatch = { bundle, patch -> vm.getOptions(bundle.uid, patch) }
|
||||
)
|
||||
@@ -154,8 +153,7 @@ fun RequiredOptionsScreen(
|
||||
value = value,
|
||||
setValue = { new ->
|
||||
vm.setOption(bundle.uid, it, key, new)
|
||||
},
|
||||
selectionWarningEnabled = vm.selectionWarningEnabled
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
package app.revanced.manager.ui.screen
|
||||
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.ArrowRight
|
||||
import androidx.compose.material.icons.filled.AutoFixHigh
|
||||
import androidx.compose.material.icons.outlined.WarningAmber
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -30,56 +32,59 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.data.platform.NetworkInfo
|
||||
import app.revanced.manager.data.room.apps.installed.InstallType
|
||||
import app.revanced.manager.data.room.apps.installed.InstalledApp
|
||||
import app.revanced.manager.network.downloader.LoadedDownloaderPlugin
|
||||
import app.revanced.manager.ui.component.AlertDialogExtended
|
||||
import app.revanced.manager.ui.component.AppInfo
|
||||
import app.revanced.manager.ui.component.AppTopBar
|
||||
import app.revanced.manager.ui.component.ColumnWithScrollbar
|
||||
import app.revanced.manager.ui.component.NotificationCard
|
||||
import app.revanced.manager.ui.component.LoadingIndicator
|
||||
import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
|
||||
import app.revanced.manager.ui.model.SelectedSource
|
||||
import app.revanced.manager.ui.model.SelectedVersion
|
||||
import app.revanced.manager.ui.model.SelectedApp
|
||||
import app.revanced.manager.ui.viewmodel.SelectedAppInfoViewModel
|
||||
import app.revanced.manager.util.EventEffect
|
||||
import app.revanced.manager.util.Options
|
||||
import app.revanced.manager.util.PatchSelection
|
||||
import app.revanced.manager.util.enabled
|
||||
import app.revanced.manager.util.patchCount
|
||||
import app.revanced.manager.util.toast
|
||||
import app.revanced.manager.util.transparentListItemColors
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.compose.koinInject
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SelectedAppInfoScreen(
|
||||
onPatchSelectorClick: (packageName: String, version: String?, PatchSelection?, Options) -> Unit,
|
||||
onRequiredOptions: (packageName: String, version: String?, PatchSelection?, Options) -> Unit,
|
||||
onPatchSelectorClick: (SelectedApp, PatchSelection?, Options) -> Unit,
|
||||
onRequiredOptions: (SelectedApp, PatchSelection?, Options) -> Unit,
|
||||
onPatchClick: () -> Unit,
|
||||
onVersionClick: (packageName: String, patchSelection: PatchSelection, selectedVersion: SelectedVersion, localPath: String?) -> Unit,
|
||||
onSourceClick: (packageName: String, version: String?, SelectedSource, localPath: String?) -> Unit,
|
||||
onBackClick: () -> Unit,
|
||||
vm: SelectedAppInfoViewModel
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val networkInfo = koinInject<NetworkInfo>()
|
||||
val networkConnected = remember { networkInfo.isConnected() }
|
||||
val networkMetered = remember { !networkInfo.isUnmetered() }
|
||||
|
||||
val packageName = vm.packageName
|
||||
val packageName = vm.selectedApp.packageName
|
||||
val version = vm.selectedApp.version
|
||||
val bundles by vm.bundleInfoFlow.collectAsStateWithLifecycle(emptyList())
|
||||
|
||||
val allowIncompatiblePatches by vm.prefs.disablePatchVersionCompatCheck.getAsState()
|
||||
val patches = remember(bundles, allowIncompatiblePatches) {
|
||||
vm.getPatches(bundles, allowIncompatiblePatches)
|
||||
}
|
||||
val selectedPatchCount = remember(patches) {
|
||||
patches.values.sumOf { it.size }
|
||||
}
|
||||
|
||||
val launcher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.StartActivityForResult(),
|
||||
onResult = vm::handlePluginActivityResult
|
||||
)
|
||||
EventEffect(flow = vm.launchActivityFlow) { intent ->
|
||||
launcher.launch(intent)
|
||||
}
|
||||
val composableScope = rememberCoroutineScope()
|
||||
|
||||
val error by vm.errorFlow.collectAsStateWithLifecycle(null)
|
||||
|
||||
val selectedVersion by vm.selectedVersion.collectAsStateWithLifecycle()
|
||||
val resolvedVersion by vm.resolvedVersion.collectAsStateWithLifecycle(null)
|
||||
|
||||
val selectedSource by vm.selectedSource.collectAsStateWithLifecycle()
|
||||
val resolvedSource by vm.resolvedSource.collectAsStateWithLifecycle(null)
|
||||
|
||||
val customSelection by vm.customSelection.collectAsStateWithLifecycle(null)
|
||||
val fullPatchSelection by vm.patchSelection.collectAsStateWithLifecycle(emptyMap())
|
||||
val patchCount = fullPatchSelection.patchCount
|
||||
|
||||
val incompatibleCount by vm.incompatiblePatchCount.collectAsStateWithLifecycle(0)
|
||||
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
|
||||
Scaffold(
|
||||
@@ -102,18 +107,18 @@ fun SelectedAppInfoScreen(
|
||||
)
|
||||
},
|
||||
onClick = patchClick@{
|
||||
if (patchCount == 0) {
|
||||
if (selectedPatchCount == 0) {
|
||||
context.toast(context.getString(R.string.no_patches_selected))
|
||||
|
||||
return@patchClick
|
||||
}
|
||||
|
||||
composableScope.launch {
|
||||
if (!vm.hasSetRequiredOptions(fullPatchSelection)) {
|
||||
if (!vm.hasSetRequiredOptions(patches)) {
|
||||
onRequiredOptions(
|
||||
vm.packageName,
|
||||
resolvedVersion,
|
||||
customSelection,
|
||||
vm.options,
|
||||
vm.selectedApp,
|
||||
vm.getCustomPatches(bundles, allowIncompatiblePatches),
|
||||
vm.options
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
@@ -125,130 +130,94 @@ fun SelectedAppInfoScreen(
|
||||
},
|
||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
) { paddingValues ->
|
||||
val plugins by vm.plugins.collectAsStateWithLifecycle(emptyList())
|
||||
|
||||
if (vm.showSourceSelector) {
|
||||
val requiredVersion by vm.requiredVersion.collectAsStateWithLifecycle(null)
|
||||
|
||||
AppSourceSelectorDialog(
|
||||
plugins = plugins,
|
||||
installedApp = vm.installedAppData,
|
||||
searchApp = SelectedApp.Search(
|
||||
vm.packageName,
|
||||
vm.desiredVersion
|
||||
),
|
||||
activeSearchJob = vm.activePluginAction,
|
||||
hasRoot = vm.hasRoot,
|
||||
onDismissRequest = vm::dismissSourceSelector,
|
||||
onSelectPlugin = vm::searchUsingPlugin,
|
||||
requiredVersion = requiredVersion,
|
||||
onSelect = {
|
||||
vm.selectedApp = it
|
||||
vm.dismissSourceSelector()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
ColumnWithScrollbar(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
) {
|
||||
AppInfo(vm.selectedAppInfo, placeholderLabel = packageName) {
|
||||
vm.selectedAppInfo?.let {
|
||||
Text(
|
||||
it.packageName,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
Text(
|
||||
version ?: stringResource(R.string.selected_app_meta_any_version),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
|
||||
PageItem(
|
||||
R.string.patch_selector_item,
|
||||
stringResource(R.string.patch_selector_item_description, patchCount),
|
||||
stringResource(
|
||||
R.string.patch_selector_item_description,
|
||||
selectedPatchCount
|
||||
),
|
||||
onClick = {
|
||||
onPatchSelectorClick(
|
||||
vm.packageName,
|
||||
resolvedVersion,
|
||||
customSelection,
|
||||
vm.selectedApp,
|
||||
vm.getCustomPatches(
|
||||
bundles,
|
||||
allowIncompatiblePatches
|
||||
),
|
||||
vm.options
|
||||
)
|
||||
},
|
||||
extraDescription = if (incompatibleCount > 0) { {
|
||||
Text(
|
||||
"$incompatibleCount incompatible",
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
} } else null,
|
||||
)
|
||||
|
||||
val versionText = resolvedVersion ?: "Any available version"
|
||||
val versionDescription = if (selectedVersion is SelectedVersion.Auto)
|
||||
"Auto ($versionText)" // stringResource(R.string.selected_app_meta_auto_version, actualVersion)
|
||||
else versionText
|
||||
|
||||
PageItem(
|
||||
R.string.version_selector_item,
|
||||
versionDescription,
|
||||
onClick = {
|
||||
onVersionClick(
|
||||
packageName,
|
||||
fullPatchSelection,
|
||||
selectedVersion,
|
||||
vm.localPath,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
val sourceText = when (val source = resolvedSource) {
|
||||
is SelectedSource.Installed -> "Installed APK"
|
||||
is SelectedSource.Downloaded -> "Downloaded APK"
|
||||
is SelectedSource.Local -> "Local APK"
|
||||
is SelectedSource.Plugin -> {
|
||||
source.packageName ?: "Any available downloader"
|
||||
}
|
||||
else -> "Auto"
|
||||
}
|
||||
val sourceDescription = if (selectedSource is SelectedSource.Auto)
|
||||
"Auto ($sourceText)" // stringResource(R.string.selected_app_meta_auto_version, actualVersion)
|
||||
else sourceText
|
||||
|
||||
)
|
||||
PageItem(
|
||||
R.string.apk_source_selector_item,
|
||||
sourceDescription,
|
||||
onClick = { onSourceClick(
|
||||
packageName,
|
||||
resolvedVersion,
|
||||
selectedSource,
|
||||
vm.localPath,
|
||||
) },
|
||||
)
|
||||
when (val app = vm.selectedApp) {
|
||||
is SelectedApp.Search -> stringResource(R.string.apk_source_auto)
|
||||
is SelectedApp.Installed -> stringResource(R.string.apk_source_installed)
|
||||
is SelectedApp.Download -> stringResource(
|
||||
R.string.apk_source_downloader,
|
||||
plugins.find { it.packageName == app.data.pluginPackageName }?.name
|
||||
?: app.data.pluginPackageName
|
||||
)
|
||||
|
||||
is SelectedApp.Local -> stringResource(R.string.apk_source_local)
|
||||
},
|
||||
onClick = {
|
||||
vm.showSourceSelector()
|
||||
}
|
||||
)
|
||||
error?.let {
|
||||
Text(
|
||||
stringResource(it.resourceId),
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.padding(horizontal = 16.dp)
|
||||
modifier = Modifier.padding(horizontal = 24.dp)
|
||||
)
|
||||
}
|
||||
|
||||
if (resolvedSource is SelectedSource.Plugin) Column(
|
||||
modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
when {
|
||||
!networkConnected -> {
|
||||
NotificationCard(
|
||||
isWarning = true,
|
||||
icon = Icons.Outlined.WarningAmber,
|
||||
text = stringResource(R.string.network_unavailable_warning),
|
||||
onDismiss = null
|
||||
)
|
||||
}
|
||||
networkMetered -> {
|
||||
NotificationCard(
|
||||
isWarning = true,
|
||||
icon = Icons.Outlined.WarningAmber,
|
||||
text = stringResource(R.string.network_metered_warning),
|
||||
onDismiss = null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PageItem(
|
||||
@StringRes title: Int,
|
||||
description: String,
|
||||
onClick: () -> Unit,
|
||||
enabled: Boolean = true,
|
||||
extraDescription: @Composable (ColumnScope.() -> Unit)? = null,
|
||||
) {
|
||||
private fun PageItem(@StringRes title: Int, description: String, onClick: () -> Unit) {
|
||||
ListItem(
|
||||
modifier = Modifier
|
||||
.clickable(enabled, onClick = onClick)
|
||||
.enabled(enabled),
|
||||
.clickable(onClick = onClick)
|
||||
.padding(start = 8.dp),
|
||||
headlineContent = {
|
||||
Text(
|
||||
stringResource(title),
|
||||
@@ -257,17 +226,99 @@ private fun PageItem(
|
||||
)
|
||||
},
|
||||
supportingContent = {
|
||||
Column {
|
||||
Text(
|
||||
description,
|
||||
color = MaterialTheme.colorScheme.outline,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
extraDescription?.invoke(this)
|
||||
}
|
||||
Text(
|
||||
description,
|
||||
color = MaterialTheme.colorScheme.outline,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
},
|
||||
trailingContent = {
|
||||
Icon(Icons.AutoMirrored.Outlined.ArrowRight, null)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AppSourceSelectorDialog(
|
||||
plugins: List<LoadedDownloaderPlugin>,
|
||||
installedApp: Pair<SelectedApp.Installed, InstalledApp?>?,
|
||||
searchApp: SelectedApp.Search,
|
||||
activeSearchJob: String?,
|
||||
hasRoot: Boolean,
|
||||
requiredVersion: String?,
|
||||
onDismissRequest: () -> Unit,
|
||||
onSelectPlugin: (LoadedDownloaderPlugin) -> Unit,
|
||||
onSelect: (SelectedApp) -> Unit,
|
||||
) {
|
||||
val canSelect = activeSearchJob == null
|
||||
|
||||
AlertDialogExtended(
|
||||
onDismissRequest = onDismissRequest,
|
||||
confirmButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(stringResource(R.string.cancel))
|
||||
}
|
||||
},
|
||||
title = { Text(stringResource(R.string.app_source_dialog_title)) },
|
||||
textHorizontalPadding = PaddingValues(horizontal = 0.dp),
|
||||
text = {
|
||||
LazyColumn {
|
||||
item(key = "auto") {
|
||||
val hasPlugins = plugins.isNotEmpty()
|
||||
ListItem(
|
||||
modifier = Modifier
|
||||
.clickable(enabled = canSelect && hasPlugins) { onSelect(searchApp) }
|
||||
.enabled(hasPlugins),
|
||||
headlineContent = { Text(stringResource(R.string.app_source_dialog_option_auto)) },
|
||||
supportingContent = {
|
||||
Text(
|
||||
if (hasPlugins)
|
||||
stringResource(R.string.app_source_dialog_option_auto_description)
|
||||
else
|
||||
stringResource(R.string.app_source_dialog_option_auto_unavailable)
|
||||
)
|
||||
},
|
||||
colors = transparentListItemColors
|
||||
)
|
||||
}
|
||||
|
||||
installedApp?.let { (app, meta) ->
|
||||
item(key = "installed") {
|
||||
val (usable, text) = when {
|
||||
// Mounted apps must be unpatched before patching, which cannot be done without root access.
|
||||
meta?.installType == InstallType.MOUNT && !hasRoot -> false to stringResource(
|
||||
R.string.app_source_dialog_option_installed_no_root
|
||||
)
|
||||
// Patching already patched apps is not allowed because patches expect unpatched apps.
|
||||
meta?.installType == InstallType.DEFAULT -> false to stringResource(R.string.already_patched)
|
||||
// Version does not match suggested version.
|
||||
requiredVersion != null && app.version != requiredVersion -> false to stringResource(
|
||||
R.string.app_source_dialog_option_installed_version_not_suggested,
|
||||
app.version
|
||||
)
|
||||
|
||||
else -> true to app.version
|
||||
}
|
||||
ListItem(
|
||||
modifier = Modifier
|
||||
.clickable(enabled = canSelect && usable) { onSelect(app) }
|
||||
.enabled(usable),
|
||||
headlineContent = { Text(stringResource(R.string.installed)) },
|
||||
supportingContent = { Text(text) },
|
||||
colors = transparentListItemColors
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
items(plugins, key = { "plugin_${it.packageName}" }) { plugin ->
|
||||
ListItem(
|
||||
modifier = Modifier.clickable(enabled = canSelect) { onSelectPlugin(plugin) },
|
||||
headlineContent = { Text(plugin.name) },
|
||||
trailingContent = (@Composable { LoadingIndicator() }).takeIf { activeSearchJob == plugin.packageName },
|
||||
colors = transparentListItemColors
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user