mirror of
https://github.com/ReVanced/revanced-manager.git
synced 2026-01-13 06:27:43 +00:00
Compare commits
328 Commits
fix/buildt
...
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
|
||||
103
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
103
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -1,103 +0,0 @@
|
||||
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.
|
||||
|
||||
33
.github/workflows/build_pull_request.yml
vendored
33
.github/workflows/build_pull_request.yml
vendored
@@ -1,33 +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
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- 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
|
||||
72
.github/workflows/release.yml
vendored
72
.github/workflows/release.yml
vendored
@@ -1,72 +0,0 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Release
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
attestations: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
|
||||
- name: Cache Gradle
|
||||
uses: burrunan/gradle-cache-action@v1
|
||||
|
||||
- name: Build
|
||||
env:
|
||||
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: Semantic Release
|
||||
uses: cycjimmy/semantic-release-action@v4
|
||||
id: semantic
|
||||
env:
|
||||
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.semantic.outputs.new_release_published == 'true'
|
||||
uses: actions/attest-build-provenance@v2
|
||||
with:
|
||||
subject-name: 'ReVanced Manager ${{ steps.release.outputs.new_release_git_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/
|
||||
|
||||
49
.releaserc
49
.releaserc
@@ -1,49 +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": "app/build/outputs/apk/release/revanced-manager*.apk?(.asc)"
|
||||
},
|
||||
],
|
||||
successComment: false
|
||||
}
|
||||
],
|
||||
[
|
||||
"@saithodev/semantic-release-backmerge",
|
||||
{
|
||||
backmergeBranches: [{"from": "main", "to": "dev"}],
|
||||
clearWorkspace: true
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
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,151 +0,0 @@
|
||||
import java.io.IOException
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
fun String.runCommand(): String {
|
||||
val process = ProcessBuilder(split("\\s".toRegex()))
|
||||
.redirectErrorStream(true)
|
||||
.directory(rootDir)
|
||||
.start()
|
||||
|
||||
val output = StringBuilder()
|
||||
val reader = process.inputStream.bufferedReader()
|
||||
|
||||
val thread = Thread {
|
||||
reader.forEachLine {
|
||||
output.appendLine(it)
|
||||
}
|
||||
}
|
||||
thread.start()
|
||||
|
||||
if (!process.waitFor(10, TimeUnit.SECONDS)) {
|
||||
process.destroy()
|
||||
throw IOException("Command timed out: $this")
|
||||
}
|
||||
|
||||
thread.join()
|
||||
return output.toString().trim()
|
||||
}
|
||||
|
||||
val projectPath: String = projectDir.relativeTo(rootDir).path
|
||||
val lastTag = "git describe --tags --abbrev=0".runCommand()
|
||||
val hasChangesInThisModule = "git diff --name-only $lastTag..HEAD".runCommand().lineSequence().any {
|
||||
it.startsWith(projectPath)
|
||||
}
|
||||
|
||||
tasks.matching { it.name.startsWith("publish") }.configureEach {
|
||||
onlyIf {
|
||||
hasChangesInThisModule
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "app.revanced.manager.plugin.downloader"
|
||||
compileSdk = 35
|
||||
buildToolsVersion = "35.0.1"
|
||||
|
||||
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"])
|
||||
}
|
||||
@@ -8,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)
|
||||
@@ -40,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)
|
||||
@@ -60,7 +161,7 @@ dependencies {
|
||||
implementation(libs.revanced.library)
|
||||
|
||||
// Downloader plugins
|
||||
implementation(project(":api"))
|
||||
implementation(project(":downloader-plugin"))
|
||||
|
||||
// Native processes
|
||||
implementation(libs.kotlin.process)
|
||||
@@ -108,145 +209,3 @@ dependencies {
|
||||
// Compose Icons
|
||||
implementation(libs.compose.icons.fontawesome)
|
||||
}
|
||||
|
||||
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 (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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -44,14 +44,13 @@ import app.revanced.manager.ui.screen.SettingsScreen
|
||||
import app.revanced.manager.ui.screen.UpdateScreen
|
||||
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
|
||||
@@ -164,7 +163,7 @@ private fun ReVancedManager(vm: MainViewModel) {
|
||||
}
|
||||
}
|
||||
},
|
||||
viewModel = koinViewModel { parametersOf(it.getComplexArg<Patcher.ViewModelParams>()) }
|
||||
vm = koinViewModel { parametersOf(it.getComplexArg<Patcher.ViewModelParams>()) }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -234,7 +233,7 @@ private fun ReVancedManager(vm: MainViewModel) {
|
||||
selectedAppInfoVm.updateConfiguration(patches, options)
|
||||
navController.popBackStack()
|
||||
},
|
||||
viewModel = koinViewModel { parametersOf(data) }
|
||||
vm = koinViewModel { parametersOf(data) }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -270,52 +269,47 @@ private fun ReVancedManager(vm: MainViewModel) {
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -15,6 +15,7 @@ val repositoryModule = module {
|
||||
createdAtStart()
|
||||
}
|
||||
singleOf(::NetworkInfo)
|
||||
singleOf(::PatchBundlePersistenceRepository)
|
||||
singleOf(::PatchSelectionRepository)
|
||||
singleOf(::PatchOptionsRepository)
|
||||
singleOf(::PatchBundleRepository) {
|
||||
|
||||
@@ -23,5 +23,4 @@ val viewModelModule = module {
|
||||
viewModelOf(::InstalledAppsViewModel)
|
||||
viewModelOf(::InstalledAppInfoViewModel)
|
||||
viewModelOf(::UpdatesSettingsViewModel)
|
||||
viewModelOf(::BundleListViewModel)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
@@ -3,7 +3,6 @@ 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
|
||||
@@ -25,10 +24,8 @@ class PreferencesManager(
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -40,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.
|
||||
@@ -98,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,78 +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.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 } }
|
||||
|
||||
val suggestedVersions = bundleInfoFlow.map {
|
||||
val suggestedVersions = bundles.map {
|
||||
val allPatches =
|
||||
it.values.flatMap { bundle -> bundle.patches.map(PatchInfo::toPatcherPatch) }.toSet()
|
||||
|
||||
@@ -97,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
|
||||
@@ -206,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()
|
||||
@@ -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()
|
||||
})
|
||||
|
||||
@@ -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,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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@ package app.revanced.manager.patcher.runtime
|
||||
import android.content.Context
|
||||
import app.revanced.manager.patcher.Session
|
||||
import app.revanced.manager.patcher.logger.Logger
|
||||
import app.revanced.manager.patcher.patch.PatchBundle
|
||||
import app.revanced.manager.patcher.worker.ProgressEventHandler
|
||||
import app.revanced.manager.ui.model.State
|
||||
import app.revanced.manager.util.Options
|
||||
@@ -24,17 +23,14 @@ class CoroutineRuntime(private val context: Context) : Runtime(context) {
|
||||
onPatchCompleted: suspend () -> Unit,
|
||||
onProgress: ProgressEventHandler,
|
||||
) {
|
||||
val selectedBundles = selectedPatches.keys
|
||||
val bundles = bundles()
|
||||
val uids = bundles.entries.associate { (key, value) -> value to key }
|
||||
|
||||
val allPatches =
|
||||
PatchBundle.Loader.patches(bundles.values, packageName)
|
||||
.mapKeys { (b, _) -> uids[b]!! }
|
||||
.filterKeys { it in selectedBundles }
|
||||
val 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 }
|
||||
allPatches[bundle]?.filter { selected.contains(it.name) }
|
||||
?: throw IllegalArgumentException("Patch bundle $bundle does not exist")
|
||||
}
|
||||
|
||||
|
||||
@@ -111,7 +111,6 @@ class ProcessRuntime(private val context: Context) : Runtime(context) {
|
||||
}
|
||||
|
||||
val patching = CompletableDeferred<Unit>()
|
||||
val scope = this
|
||||
|
||||
launch(Dispatchers.IO) {
|
||||
val binder = awaitBinderConnection()
|
||||
@@ -125,7 +124,7 @@ class ProcessRuntime(private val context: Context) : Runtime(context) {
|
||||
override fun log(level: String, msg: String) = logger.log(enumValueOf(level), msg)
|
||||
|
||||
override fun patchSucceeded() {
|
||||
scope.launch { onPatchCompleted() }
|
||||
launch { onPatchCompleted() }
|
||||
}
|
||||
|
||||
override fun progress(name: String?, state: String?, msg: String?) =
|
||||
@@ -142,6 +141,8 @@ class ProcessRuntime(private val context: Context) : Runtime(context) {
|
||||
}
|
||||
}
|
||||
|
||||
val bundles = bundles()
|
||||
|
||||
val parameters = Parameters(
|
||||
aaptPath = aaptPath,
|
||||
frameworkDir = frameworkPath,
|
||||
@@ -149,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()
|
||||
)
|
||||
}
|
||||
)
|
||||
@@ -176,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].
|
||||
*/
|
||||
|
||||
@@ -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,10 +1,8 @@
|
||||
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
|
||||
@@ -56,10 +54,11 @@ class PatcherProcess(private val context: Context) : IPatcherProcess.Stub() {
|
||||
|
||||
logger.info("Memory limit: ${Runtime.getRuntime().maxMemory() / (1024 * 1024)}MB")
|
||||
|
||||
val allPatches = PatchBundle.Loader.patches(parameters.configurations.map { it.bundle }, parameters.packageName)
|
||||
val patchList = parameters.configurations.flatMap { config ->
|
||||
val patches = (allPatches[config.bundle] ?: return@flatMap emptyList())
|
||||
.filter { it.name in config.patches }
|
||||
val bundle = PatchBundle(File(config.bundlePath))
|
||||
|
||||
val patches =
|
||||
bundle.patches(parameters.packageName).filter { it.name in config.patches }
|
||||
.associateBy { it.name }
|
||||
|
||||
config.options.forEach { (patchName, opts) ->
|
||||
@@ -96,10 +95,6 @@ class PatcherProcess(private val context: Context) : 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()
|
||||
@@ -110,15 +105,6 @@ class PatcherProcess(private val context: Context) : 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(appContext)
|
||||
|
||||
appContext.sendBroadcast(Intent().apply {
|
||||
|
||||
@@ -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
|
||||
@@ -88,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 {
|
||||
@@ -161,8 +158,6 @@ class PatcherWorker(
|
||||
data,
|
||||
args.packageName,
|
||||
args.input.version,
|
||||
prefs.suggestedVersionSafeguard.get(),
|
||||
!prefs.disablePatchVersionCompatCheck.get(),
|
||||
onDownload = args.onDownloadProgress
|
||||
).also {
|
||||
args.setInputFile(it)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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,99 +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.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 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(
|
||||
@@ -102,7 +73,7 @@ fun BundleInformationDialog(
|
||||
)
|
||||
},
|
||||
actions = {
|
||||
if (!src.isDefault) {
|
||||
if (!bundle.isDefault) {
|
||||
IconButton(onClick = onDeleteRequest) {
|
||||
Icon(
|
||||
Icons.Outlined.DeleteOutline,
|
||||
@@ -110,7 +81,7 @@ fun BundleInformationDialog(
|
||||
)
|
||||
}
|
||||
}
|
||||
if (!isLocal && hasNetwork) {
|
||||
if (!isLocal) {
|
||||
IconButton(onClick = onUpdate) {
|
||||
Icon(
|
||||
Icons.Outlined.Update,
|
||||
@@ -122,175 +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() }
|
||||
)
|
||||
|
||||
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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -70,9 +70,6 @@ object Settings {
|
||||
@Serializable
|
||||
data object Advanced : Destination
|
||||
|
||||
@Serializable
|
||||
data object Updates : Destination
|
||||
|
||||
@Serializable
|
||||
data object Downloads : Destination
|
||||
|
||||
@@ -92,5 +89,5 @@ object Settings {
|
||||
data object Licenses : Destination
|
||||
|
||||
@Serializable
|
||||
data object Developer : Destination
|
||||
data object DeveloperOptions : Destination
|
||||
}
|
||||
@@ -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
|
||||
@@ -60,34 +46,25 @@ import app.revanced.manager.util.EventEffect
|
||||
@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() {
|
||||
if(patcherSucceeded == null)
|
||||
showDismissConfirmationDialog = true
|
||||
else
|
||||
onLeave()
|
||||
}
|
||||
|
||||
BackHandler(onBack = ::onPageBack)
|
||||
|
||||
val steps by remember {
|
||||
derivedStateOf {
|
||||
viewModel.steps.groupBy { it.category }
|
||||
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()
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -214,7 +181,7 @@ fun PatcherScreen(
|
||||
.fillMaxSize()
|
||||
) {
|
||||
LinearProgressIndicator(
|
||||
progress = { viewModel.progress },
|
||||
progress = { vm.progress },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
@@ -230,11 +197,11 @@ fun PatcherScreen(
|
||||
Steps(
|
||||
category = category,
|
||||
steps = steps,
|
||||
stepCount = if (category == StepCategory.PATCHING) viewModel.patchesProgress else null,
|
||||
stepProgressProvider = viewModel
|
||||
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 = { onToggle() },
|
||||
enabled = compatible
|
||||
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
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,6 @@ 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.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
@@ -14,7 +12,6 @@ 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
|
||||
@@ -35,7 +32,6 @@ 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
|
||||
@@ -44,7 +40,6 @@ 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.LoadingIndicator
|
||||
import app.revanced.manager.ui.component.NotificationCard
|
||||
import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
|
||||
import app.revanced.manager.ui.model.SelectedApp
|
||||
import app.revanced.manager.ui.viewmodel.SelectedAppInfoViewModel
|
||||
@@ -55,7 +50,6 @@ import app.revanced.manager.util.enabled
|
||||
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
|
||||
@@ -67,9 +61,6 @@ fun SelectedAppInfoScreen(
|
||||
vm: SelectedAppInfoViewModel
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val networkInfo = koinInject<NetworkInfo>()
|
||||
val networkConnected = remember { networkInfo.isConnected() }
|
||||
val networkMetered = remember { !networkInfo.isUnmetered() }
|
||||
|
||||
val packageName = vm.selectedApp.packageName
|
||||
val version = vm.selectedApp.version
|
||||
@@ -217,35 +208,6 @@ fun SelectedAppInfoScreen(
|
||||
modifier = Modifier.padding(horizontal = 24.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier.padding(horizontal = 24.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
val needsInternet =
|
||||
vm.selectedApp.let { it is SelectedApp.Search || it is SelectedApp.Download }
|
||||
|
||||
when {
|
||||
!needsInternet -> {}
|
||||
!networkConnected -> {
|
||||
NotificationCard(
|
||||
isWarning = true,
|
||||
icon = Icons.Outlined.WarningAmber,
|
||||
text = stringResource(R.string.network_unavailable_warning),
|
||||
onDismiss = null
|
||||
)
|
||||
}
|
||||
|
||||
networkMetered -> {
|
||||
NotificationCard(
|
||||
isWarning = true,
|
||||
icon = Icons.Outlined.WarningAmber,
|
||||
text = stringResource(R.string.network_metered_warning),
|
||||
onDismiss = null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,86 +1,57 @@
|
||||
package app.revanced.manager.ui.screen
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.material.icons.outlined.Download
|
||||
import androidx.compose.material.icons.outlined.Info
|
||||
import androidx.compose.material.icons.outlined.Settings
|
||||
import androidx.compose.material.icons.outlined.SwapVert
|
||||
import androidx.compose.material.icons.outlined.Tune
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.domain.manager.PreferencesManager
|
||||
import app.revanced.manager.ui.component.AppTopBar
|
||||
import app.revanced.manager.ui.component.ColumnWithScrollbar
|
||||
import app.revanced.manager.ui.component.settings.SettingsListItem
|
||||
import app.revanced.manager.ui.model.navigation.Settings
|
||||
import org.koin.compose.koinInject
|
||||
|
||||
private data class Section(
|
||||
@StringRes val name: Int,
|
||||
@StringRes val description: Int,
|
||||
val image: ImageVector,
|
||||
val destination: Settings.Destination,
|
||||
private val settingsSections = listOf(
|
||||
Triple(
|
||||
R.string.general,
|
||||
R.string.general_description,
|
||||
Icons.Outlined.Settings
|
||||
) to Settings.General,
|
||||
Triple(
|
||||
R.string.extensions,
|
||||
R.string.extensions_description,
|
||||
Icons.Outlined.Download
|
||||
) to Settings.Downloads,
|
||||
Triple(
|
||||
R.string.backup_restore,
|
||||
R.string.backup_restore_description,
|
||||
Icons.Outlined.SwapVert
|
||||
) to Settings.ImportExport,
|
||||
Triple(
|
||||
R.string.advanced,
|
||||
R.string.advanced_description,
|
||||
Icons.Outlined.Tune
|
||||
) to Settings.Advanced,
|
||||
Triple(
|
||||
R.string.about,
|
||||
R.string.app_name,
|
||||
Icons.Outlined.Info
|
||||
) to Settings.About,
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SettingsScreen(onBackClick: () -> Unit, navigate: (Settings.Destination) -> Unit) {
|
||||
val prefs: PreferencesManager = koinInject()
|
||||
val showDeveloperSettings by prefs.showDeveloperSettings.getAsState()
|
||||
|
||||
val settingsSections = remember(showDeveloperSettings) {
|
||||
listOfNotNull(
|
||||
Section(
|
||||
R.string.general,
|
||||
R.string.general_description,
|
||||
Icons.Outlined.Settings,
|
||||
Settings.General
|
||||
),
|
||||
Section(
|
||||
R.string.updates,
|
||||
R.string.updates_description,
|
||||
Icons.Outlined.Update,
|
||||
Settings.Updates
|
||||
),
|
||||
Section(
|
||||
R.string.downloads,
|
||||
R.string.downloads_description,
|
||||
Icons.Outlined.Download,
|
||||
Settings.Downloads
|
||||
),
|
||||
Section(
|
||||
R.string.import_export,
|
||||
R.string.import_export_description,
|
||||
Icons.Outlined.SwapVert,
|
||||
Settings.ImportExport
|
||||
),
|
||||
Section(
|
||||
R.string.advanced,
|
||||
R.string.advanced_description,
|
||||
Icons.Outlined.Tune,
|
||||
Settings.Advanced
|
||||
),
|
||||
Section(
|
||||
R.string.about,
|
||||
R.string.app_name,
|
||||
Icons.Outlined.Info,
|
||||
Settings.About
|
||||
),
|
||||
Section(
|
||||
R.string.developer_options,
|
||||
R.string.developer_options_description,
|
||||
Icons.Outlined.Code,
|
||||
Settings.Developer
|
||||
).takeIf { showDeveloperSettings }
|
||||
)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
AppTopBar(
|
||||
@@ -94,12 +65,12 @@ fun SettingsScreen(onBackClick: () -> Unit, navigate: (Settings.Destination) ->
|
||||
.padding(paddingValues)
|
||||
.fillMaxSize()
|
||||
) {
|
||||
settingsSections.forEach { (name, description, icon, destination) ->
|
||||
settingsSections.forEach { (titleDescIcon, destination) ->
|
||||
SettingsListItem(
|
||||
modifier = Modifier.clickable { navigate(destination) },
|
||||
headlineContent = stringResource(name),
|
||||
supportingContent = stringResource(description),
|
||||
leadingContent = { Icon(icon, null) }
|
||||
headlineContent = stringResource(titleDescIcon.first),
|
||||
supportingContent = stringResource(titleDescIcon.second),
|
||||
leadingContent = { Icon(titleDescIcon.third, null) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,17 +5,19 @@ import androidx.compose.animation.core.spring
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Cancel
|
||||
import androidx.compose.material.icons.outlined.InstallMobile
|
||||
import androidx.compose.material.icons.outlined.Update
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@@ -26,15 +28,16 @@ import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import app.revanced.manager.BuildConfig
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.network.dto.ReVancedAsset
|
||||
import app.revanced.manager.ui.component.AppTopBar
|
||||
import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
|
||||
import app.revanced.manager.ui.component.settings.Changelog
|
||||
import app.revanced.manager.ui.viewmodel.UpdateViewModel
|
||||
import app.revanced.manager.ui.viewmodel.UpdateViewModel.State
|
||||
@@ -57,81 +60,37 @@ fun UpdateScreen(
|
||||
Scaffold(
|
||||
topBar = {
|
||||
AppTopBar(
|
||||
title = {
|
||||
Column {
|
||||
Text(stringResource(vm.state.title))
|
||||
|
||||
if (vm.state == State.DOWNLOADING) {
|
||||
Text(
|
||||
text = "${vm.downloadedSize.div(1000000)} MB / ${
|
||||
vm.totalSize.div(1000000)
|
||||
} MB (${vm.downloadProgress.times(100).toInt()}%)",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.outline
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
title = stringResource(R.string.update),
|
||||
scrollBehavior = scrollBehavior,
|
||||
onBackClick = onBackClick
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
val buttonConfig = when (vm.state) {
|
||||
State.CAN_DOWNLOAD -> Triple(
|
||||
{ vm.downloadUpdate() },
|
||||
R.string.download,
|
||||
Icons.Outlined.InstallMobile
|
||||
)
|
||||
|
||||
State.DOWNLOADING -> Triple(onBackClick, R.string.cancel, Icons.Outlined.Cancel)
|
||||
State.CAN_INSTALL -> Triple(
|
||||
{ vm.installUpdate() },
|
||||
R.string.install_app,
|
||||
Icons.Outlined.InstallMobile
|
||||
)
|
||||
|
||||
else -> null
|
||||
}
|
||||
|
||||
buttonConfig?.let { (onClick, textRes, icon) ->
|
||||
HapticExtendedFloatingActionButton(
|
||||
onClick = onClick::invoke,
|
||||
icon = { Icon(icon, null) },
|
||||
text = { Text(stringResource(textRes)) }
|
||||
)
|
||||
}
|
||||
|
||||
},
|
||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
) { paddingValues ->
|
||||
AnimatedVisibility(visible = vm.showInternetCheckDialog) {
|
||||
MeteredDownloadConfirmationDialog(
|
||||
onDismiss = { vm.showInternetCheckDialog = false },
|
||||
onDownloadAnyways = { vm.downloadUpdate(true) }
|
||||
)
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues),
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.padding(vertical = 16.dp, horizontal = 24.dp)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.spacedBy(32.dp)
|
||||
) {
|
||||
if (vm.state == State.DOWNLOADING)
|
||||
LinearProgressIndicator(
|
||||
progress = { vm.downloadProgress },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
|
||||
AnimatedVisibility(visible = vm.showInternetCheckDialog) {
|
||||
MeteredDownloadConfirmationDialog(
|
||||
onDismiss = { vm.showInternetCheckDialog = false },
|
||||
onDownloadAnyways = { vm.downloadUpdate(true) }
|
||||
)
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.spacedBy(32.dp)
|
||||
) {
|
||||
vm.releaseInfo?.let { changelog ->
|
||||
Changelog(changelog)
|
||||
}
|
||||
}
|
||||
Header(
|
||||
vm.state,
|
||||
vm.releaseInfo,
|
||||
DownloadData(vm.downloadProgress, vm.downloadedSize, vm.totalSize)
|
||||
)
|
||||
vm.releaseInfo?.let { changelog ->
|
||||
HorizontalDivider()
|
||||
Changelog(changelog)
|
||||
} ?: Spacer(modifier = Modifier.weight(1f))
|
||||
Buttons(vm.state, vm::downloadUpdate, vm::installUpdate, onBackClick)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -164,6 +123,58 @@ private fun MeteredDownloadConfirmationDialog(
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Header(state: State, releaseInfo: ReVancedAsset?, downloadData: DownloadData) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
Text(
|
||||
text = stringResource(state.title),
|
||||
style = MaterialTheme.typography.headlineMedium
|
||||
)
|
||||
if (state == State.CAN_DOWNLOAD) {
|
||||
Column {
|
||||
Text(
|
||||
text = stringResource(
|
||||
id = R.string.current_version,
|
||||
BuildConfig.VERSION_NAME
|
||||
),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
releaseInfo?.version?.let {
|
||||
Text(
|
||||
text = stringResource(
|
||||
R.string.new_version,
|
||||
it.replace("v", "")
|
||||
),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
} else if (state == State.DOWNLOADING) {
|
||||
LinearProgressIndicator(
|
||||
progress = { downloadData.downloadProgress },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
Text(
|
||||
text =
|
||||
"${downloadData.downloadedSize.div(1000000)} MB / ${
|
||||
downloadData.totalSize.div(
|
||||
1000000
|
||||
)
|
||||
} MB (${
|
||||
downloadData.downloadProgress.times(
|
||||
100
|
||||
).toInt()
|
||||
}%)",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.outline,
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ColumnScope.Changelog(releaseInfo: ReVancedAsset) {
|
||||
val scrollState = rememberScrollState()
|
||||
@@ -194,4 +205,40 @@ private fun ColumnScope.Changelog(releaseInfo: ReVancedAsset) {
|
||||
publishDate = releaseInfo.createdAt.relativeTime(LocalContext.current)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Buttons(
|
||||
state: State,
|
||||
onDownloadClick: () -> Unit,
|
||||
onInstallClick: () -> Unit,
|
||||
onBackClick: () -> Unit
|
||||
) {
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
if (state.showCancel) {
|
||||
TextButton(
|
||||
onClick = onBackClick,
|
||||
) {
|
||||
Text(text = stringResource(R.string.cancel))
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
if (state == State.CAN_DOWNLOAD) {
|
||||
Button(onClick = onDownloadClick) {
|
||||
Text(text = stringResource(R.string.update))
|
||||
}
|
||||
} else if (state == State.CAN_INSTALL) {
|
||||
Button(
|
||||
onClick = onInstallClick
|
||||
) {
|
||||
Text(text = stringResource(R.string.install_app))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class DownloadData(
|
||||
val downloadProgress: Float,
|
||||
val downloadedSize: Long,
|
||||
val totalSize: Long
|
||||
)
|
||||
@@ -23,26 +23,16 @@ import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedCard
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarDuration
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.hideFromAccessibility
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.unit.dp
|
||||
import app.revanced.manager.BuildConfig
|
||||
import app.revanced.manager.R
|
||||
@@ -52,10 +42,8 @@ import app.revanced.manager.ui.component.ColumnWithScrollbar
|
||||
import app.revanced.manager.ui.component.settings.SettingsListItem
|
||||
import app.revanced.manager.ui.model.navigation.Settings
|
||||
import app.revanced.manager.ui.viewmodel.AboutViewModel
|
||||
import app.revanced.manager.ui.viewmodel.AboutViewModel.Companion.DEVELOPER_OPTIONS_TAPS
|
||||
import app.revanced.manager.ui.viewmodel.AboutViewModel.Companion.getSocialIcon
|
||||
import app.revanced.manager.util.openUrl
|
||||
import app.revanced.manager.util.toast
|
||||
import com.google.accompanist.drawablepainter.rememberDrawablePainter
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
|
||||
@@ -63,6 +51,7 @@ import org.koin.androidx.compose.koinViewModel
|
||||
@Composable
|
||||
fun AboutSettingsScreen(
|
||||
onBackClick: () -> Unit,
|
||||
onChangelogClick: () -> Unit,
|
||||
navigate: (Settings.Destination) -> Unit,
|
||||
viewModel: AboutViewModel = koinViewModel()
|
||||
) {
|
||||
@@ -121,7 +110,11 @@ fun AboutSettingsScreen(
|
||||
|
||||
val listItems = listOfNotNull(
|
||||
Triple(
|
||||
stringResource(R.string.submit_feedback),
|
||||
stringResource(R.string.changelog),
|
||||
stringResource(R.string.changelog_description),
|
||||
third = { onChangelogClick }
|
||||
),
|
||||
Triple(stringResource(R.string.submit_feedback),
|
||||
stringResource(R.string.submit_feedback_description),
|
||||
third = {
|
||||
context.openUrl("https://github.com/ReVanced/revanced-manager/issues/new/choose")
|
||||
@@ -129,14 +122,12 @@ fun AboutSettingsScreen(
|
||||
Triple(
|
||||
stringResource(R.string.contributors),
|
||||
stringResource(R.string.contributors_description),
|
||||
third = nav@{
|
||||
if (!viewModel.isConnected) {
|
||||
context.toast(context.getString(R.string.no_network_toast))
|
||||
return@nav
|
||||
}
|
||||
|
||||
navigate(Settings.Contributors)
|
||||
}
|
||||
third = { navigate(Settings.Contributors) }
|
||||
),
|
||||
Triple(
|
||||
stringResource(R.string.developer_options),
|
||||
stringResource(R.string.developer_options_description),
|
||||
third = { navigate(Settings.DeveloperOptions) }
|
||||
),
|
||||
Triple(
|
||||
stringResource(R.string.opensource_licenses),
|
||||
@@ -146,35 +137,6 @@ fun AboutSettingsScreen(
|
||||
)
|
||||
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
|
||||
val showDeveloperSettings by viewModel.showDeveloperSettings.getAsState()
|
||||
var developerTaps by rememberSaveable { mutableIntStateOf(0) }
|
||||
LaunchedEffect(developerTaps) {
|
||||
if (developerTaps == 0) return@LaunchedEffect
|
||||
if (showDeveloperSettings) {
|
||||
snackbarHostState.showSnackbar(context.getString(R.string.developer_options_already_enabled))
|
||||
developerTaps = 0
|
||||
return@LaunchedEffect
|
||||
}
|
||||
|
||||
val remaining = DEVELOPER_OPTIONS_TAPS - developerTaps
|
||||
if (remaining > 0) {
|
||||
snackbarHostState.showSnackbar(
|
||||
context.getString(
|
||||
R.string.developer_options_taps,
|
||||
remaining
|
||||
),
|
||||
duration = SnackbarDuration.Long
|
||||
)
|
||||
} else if (remaining == 0) {
|
||||
viewModel.showDeveloperSettings.update(true)
|
||||
snackbarHostState.showSnackbar(context.getString(R.string.developer_options_enabled))
|
||||
}
|
||||
|
||||
// Reset the counter
|
||||
developerTaps = 0
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
@@ -184,9 +146,6 @@ fun AboutSettingsScreen(
|
||||
onBackClick = onBackClick
|
||||
)
|
||||
},
|
||||
snackbarHost = {
|
||||
SnackbarHost(hostState = snackbarHostState)
|
||||
},
|
||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
) { paddingValues ->
|
||||
ColumnWithScrollbar(
|
||||
@@ -197,11 +156,9 @@ fun AboutSettingsScreen(
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Image(
|
||||
modifier = Modifier
|
||||
.padding(top = 16.dp)
|
||||
.clickable { developerTaps += 1 },
|
||||
modifier = Modifier.padding(top = 16.dp),
|
||||
painter = icon,
|
||||
contentDescription = stringResource(R.string.app_name)
|
||||
contentDescription = null
|
||||
)
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
@@ -209,11 +166,7 @@ fun AboutSettingsScreen(
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.app_name),
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
modifier = Modifier.semantics {
|
||||
// Icon already has this information for the purpose of being clickable.
|
||||
hideFromAccessibility()
|
||||
}
|
||||
style = MaterialTheme.typography.headlineSmall
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.version) + " " + BuildConfig.VERSION_NAME + " (" + BuildConfig.VERSION_CODE + ")",
|
||||
|
||||
@@ -17,6 +17,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Api
|
||||
import androidx.compose.material.icons.outlined.Edit
|
||||
import androidx.compose.material.icons.outlined.Restore
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
@@ -61,7 +62,7 @@ import org.koin.androidx.compose.koinViewModel
|
||||
@Composable
|
||||
fun AdvancedSettingsScreen(
|
||||
onBackClick: () -> Unit,
|
||||
viewModel: AdvancedSettingsViewModel = koinViewModel()
|
||||
vm: AdvancedSettingsViewModel = koinViewModel()
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val memoryLimit = remember {
|
||||
@@ -91,79 +92,95 @@ fun AdvancedSettingsScreen(
|
||||
) {
|
||||
GroupHeader(stringResource(R.string.manager))
|
||||
|
||||
val apiUrl by viewModel.prefs.api.getAsState()
|
||||
val apiUrl by vm.prefs.api.getAsState()
|
||||
var showApiUrlDialog by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
if (showApiUrlDialog) {
|
||||
APIUrlDialog(
|
||||
currentUrl = apiUrl,
|
||||
defaultUrl = viewModel.prefs.api.default,
|
||||
defaultUrl = vm.prefs.api.default,
|
||||
onSubmit = {
|
||||
showApiUrlDialog = false
|
||||
it?.let(viewModel::setApiUrl)
|
||||
it?.let(vm::setApiSource)
|
||||
}
|
||||
)
|
||||
}
|
||||
SettingsListItem(
|
||||
headlineContent = stringResource(R.string.api_url),
|
||||
supportingContent = stringResource(R.string.api_url_description),
|
||||
modifier = Modifier.clickable {
|
||||
showApiUrlDialog = true
|
||||
}
|
||||
)
|
||||
|
||||
GroupHeader(stringResource(R.string.safeguards))
|
||||
SafeguardBooleanItem(
|
||||
preference = viewModel.prefs.disablePatchVersionCompatCheck,
|
||||
coroutineScope = viewModel.viewModelScope,
|
||||
headline = R.string.patch_compat_check,
|
||||
description = R.string.patch_compat_check_description,
|
||||
confirmationText = R.string.patch_compat_check_confirmation
|
||||
)
|
||||
SafeguardBooleanItem(
|
||||
preference = viewModel.prefs.suggestedVersionSafeguard,
|
||||
coroutineScope = viewModel.viewModelScope,
|
||||
headline = R.string.suggested_version_safeguard,
|
||||
description = R.string.suggested_version_safeguard_description,
|
||||
confirmationText = R.string.suggested_version_safeguard_confirmation
|
||||
)
|
||||
SafeguardBooleanItem(
|
||||
preference = viewModel.prefs.disableSelectionWarning,
|
||||
coroutineScope = viewModel.viewModelScope,
|
||||
headline = R.string.patch_selection_safeguard,
|
||||
description = R.string.patch_selection_safeguard_description,
|
||||
confirmationText = R.string.patch_selection_safeguard_confirmation
|
||||
)
|
||||
SafeguardBooleanItem(
|
||||
preference = viewModel.prefs.disableUniversalPatchCheck,
|
||||
coroutineScope = viewModel.viewModelScope,
|
||||
headline = R.string.universal_patches_safeguard,
|
||||
description = R.string.universal_patches_safeguard_description,
|
||||
confirmationText = R.string.universal_patches_safeguard_confirmation
|
||||
)
|
||||
|
||||
GroupHeader(stringResource(R.string.patcher))
|
||||
BooleanItem(
|
||||
preference = viewModel.prefs.useProcessRuntime,
|
||||
coroutineScope = viewModel.viewModelScope,
|
||||
preference = vm.prefs.useProcessRuntime,
|
||||
coroutineScope = vm.viewModelScope,
|
||||
headline = R.string.process_runtime,
|
||||
description = R.string.process_runtime_description,
|
||||
)
|
||||
IntegerItem(
|
||||
preference = viewModel.prefs.patcherProcessMemoryLimit,
|
||||
coroutineScope = viewModel.viewModelScope,
|
||||
preference = vm.prefs.patcherProcessMemoryLimit,
|
||||
coroutineScope = vm.viewModelScope,
|
||||
headline = R.string.process_runtime_memory_limit,
|
||||
description = R.string.process_runtime_memory_limit_description,
|
||||
)
|
||||
|
||||
GroupHeader(stringResource(R.string.manager))
|
||||
SettingsListItem(
|
||||
headlineContent = stringResource(R.string.api_url),
|
||||
supportingContent = apiUrl,
|
||||
modifier = Modifier.clickable {
|
||||
showApiUrlDialog = true
|
||||
},
|
||||
trailingContent = {
|
||||
IconButton(onClick = { showApiUrlDialog = true }) {
|
||||
Icon(
|
||||
Icons.Outlined.Edit,
|
||||
contentDescription = stringResource(R.string.edit)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
SafeguardBooleanItem(
|
||||
preference = vm.prefs.disablePatchVersionCompatCheck,
|
||||
coroutineScope = vm.viewModelScope,
|
||||
headline = R.string.allow_compatibility_mixing,
|
||||
description = R.string.allow_compatibility_mixing_description,
|
||||
confirmationText = R.string.allow_compatibility_mixing_confirmation
|
||||
)
|
||||
SafeguardBooleanItem(
|
||||
preference = vm.prefs.disableUniversalPatchWarning,
|
||||
coroutineScope = vm.viewModelScope,
|
||||
headline = R.string.universal_patches_safeguard,
|
||||
description = R.string.universal_patches_safeguard_description,
|
||||
confirmationText = R.string.universal_patches_safeguard_confirmation
|
||||
)
|
||||
SafeguardBooleanItem(
|
||||
preference = vm.prefs.disableSelectionWarning,
|
||||
coroutineScope = vm.viewModelScope,
|
||||
headline = R.string.patch_selection_safeguard,
|
||||
description = R.string.patch_selection_safeguard_description,
|
||||
confirmationText = R.string.patch_selection_safeguard_confirmation
|
||||
)
|
||||
|
||||
GroupHeader(stringResource(R.string.update))
|
||||
BooleanItem(
|
||||
preference = vm.prefs.showManagerUpdateDialogOnLaunch,
|
||||
headline = R.string.show_manager_update_dialog_on_launch,
|
||||
description = R.string.check_for_update_auto_description
|
||||
)
|
||||
|
||||
GroupHeader(stringResource(R.string.experimental_features))
|
||||
BooleanItem(
|
||||
preference = vm.prefs.useProcessRuntime,
|
||||
coroutineScope = vm.viewModelScope,
|
||||
headline = R.string.process_runtime,
|
||||
description = R.string.process_runtime_description,
|
||||
)
|
||||
|
||||
GroupHeader(stringResource(R.string.debugging))
|
||||
val exportDebugLogsLauncher =
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) {
|
||||
it?.let(viewModel::exportDebugLogs)
|
||||
it?.let(vm::exportDebugLogs)
|
||||
}
|
||||
SettingsListItem(
|
||||
headlineContent = stringResource(R.string.debug_logs_export),
|
||||
modifier = Modifier.clickable { exportDebugLogsLauncher.launch(viewModel.debugLogFileName) }
|
||||
modifier = Modifier.clickable { exportDebugLogsLauncher.launch(vm.debugLogFileName) }
|
||||
)
|
||||
val clipboard = remember { context.getSystemService<ClipboardManager>()!! }
|
||||
val deviceContent = """
|
||||
|
||||
@@ -0,0 +1,381 @@
|
||||
package app.revanced.manager.ui.screen.settings
|
||||
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Key
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.ui.component.AppTopBar
|
||||
import app.revanced.manager.ui.component.ColumnWithScrollbar
|
||||
import app.revanced.manager.ui.component.GroupHeader
|
||||
import app.revanced.manager.ui.component.PasswordField
|
||||
import app.revanced.manager.ui.component.bundle.BundleSelector
|
||||
import app.revanced.manager.ui.component.settings.SettingsListItem
|
||||
import app.revanced.manager.ui.viewmodel.ImportExportViewModel
|
||||
import app.revanced.manager.util.toast
|
||||
import app.revanced.manager.util.uiSafe
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun BackupRestoreSettingsScreen(
|
||||
onBackClick: () -> Unit,
|
||||
viewModel: ImportExportViewModel = koinViewModel()
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val importKeystoreLauncher =
|
||||
rememberLauncherForActivityResult(contract = ActivityResultContracts.GetContent()) {
|
||||
it?.let { uri -> viewModel.startKeystoreImport(uri) }
|
||||
}
|
||||
val exportKeystoreLauncher =
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("*/*")) {
|
||||
it?.let(viewModel::exportKeystore)
|
||||
}
|
||||
|
||||
val patchBundles by viewModel.patchBundles.collectAsStateWithLifecycle(initialValue = emptyList())
|
||||
val packagesWithOptions by viewModel.packagesWithOptions.collectAsStateWithLifecycle(initialValue = emptySet())
|
||||
|
||||
viewModel.selectionAction?.let { action ->
|
||||
val launcher = rememberLauncherForActivityResult(action.activityContract) { uri ->
|
||||
if (uri == null) {
|
||||
viewModel.clearSelectionAction()
|
||||
} else {
|
||||
viewModel.executeSelectionAction(uri)
|
||||
}
|
||||
}
|
||||
|
||||
if (viewModel.selectedBundle == null) {
|
||||
BundleSelector(patchBundles) {
|
||||
if (it == null) {
|
||||
viewModel.clearSelectionAction()
|
||||
} else {
|
||||
viewModel.selectBundle(it)
|
||||
launcher.launch(action.activityArg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (viewModel.showCredentialsDialog) {
|
||||
KeystoreCredentialsDialog(
|
||||
onDismissRequest = viewModel::cancelKeystoreImport,
|
||||
onSubmit = { cn, pass ->
|
||||
viewModel.viewModelScope.launch {
|
||||
uiSafe(context, R.string.failed_to_import_keystore, "Failed to import keystore") {
|
||||
val result = viewModel.tryKeystoreImport(cn, pass)
|
||||
if (!result) context.toast(context.getString(R.string.restore_keystore_wrong_credentials))
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
AppTopBar(
|
||||
title = stringResource(R.string.backup_restore),
|
||||
scrollBehavior = scrollBehavior,
|
||||
onBackClick = onBackClick
|
||||
)
|
||||
},
|
||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
) { paddingValues ->
|
||||
ColumnWithScrollbar(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
) {
|
||||
var showPackageSelector by rememberSaveable {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
var showBundleSelector by rememberSaveable {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
if (showPackageSelector) {
|
||||
PackageSelector(packages = packagesWithOptions) { selected ->
|
||||
selected?.let(viewModel::resetOptionsForPackage)
|
||||
|
||||
showPackageSelector = false
|
||||
}
|
||||
}
|
||||
|
||||
if (showBundleSelector) {
|
||||
BundleSelector(bundles = patchBundles) { bundle ->
|
||||
bundle?.let(viewModel::clearOptionsForBundle)
|
||||
|
||||
showBundleSelector = false
|
||||
}
|
||||
}
|
||||
|
||||
GroupHeader(stringResource(R.string.keystore))
|
||||
GroupItem(
|
||||
onClick = {
|
||||
if (!viewModel.canExport()) {
|
||||
context.toast(context.getString(R.string.backup_keystore_unavailable))
|
||||
return@GroupItem
|
||||
}
|
||||
exportKeystoreLauncher.launch("Manager.keystore")
|
||||
},
|
||||
headline = R.string.backup,
|
||||
description = R.string.backup_keystore_description
|
||||
)
|
||||
GroupItem(
|
||||
onClick = {
|
||||
importKeystoreLauncher.launch("*/*")
|
||||
},
|
||||
headline = R.string.restore,
|
||||
description = R.string.restore_keystore_description
|
||||
)
|
||||
GroupItem(
|
||||
onClick = viewModel::regenerateKeystore,
|
||||
headline = {
|
||||
Text(
|
||||
stringResource(R.string.regenerate_keystore),
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
},
|
||||
description = R.string.regenerate_keystore_description
|
||||
)
|
||||
|
||||
GroupHeader(stringResource(R.string.patch_selection))
|
||||
GroupItem(
|
||||
onClick = viewModel::exportSelection,
|
||||
headline = R.string.backup,
|
||||
description = R.string.restore_patch_selection_description
|
||||
)
|
||||
GroupItem(
|
||||
onClick = viewModel::importSelection,
|
||||
headline = R.string.restore,
|
||||
description = R.string.backup_patch_selection_description
|
||||
)
|
||||
GroupItem(
|
||||
onClick = viewModel::resetSelection, // TODO: allow resetting selection for specific bundle or package name.
|
||||
headline = {
|
||||
Text(
|
||||
stringResource(R.string.reset),
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
},
|
||||
description = R.string.reset_patch_selection_description
|
||||
)
|
||||
|
||||
GroupHeader(stringResource(R.string.patch_options))
|
||||
// TODO: patch options import/export.
|
||||
GroupItem(
|
||||
onClick = viewModel::resetOptions,
|
||||
headline = {
|
||||
Text(
|
||||
stringResource(R.string.reset),
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
},
|
||||
description = R.string.patch_options_reset_all_description,
|
||||
)
|
||||
GroupItem(
|
||||
onClick = { showPackageSelector = true },
|
||||
headline = {
|
||||
Text(
|
||||
stringResource(R.string.patch_options_reset_package),
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
},
|
||||
description = R.string.patch_options_reset_package_description
|
||||
)
|
||||
if (patchBundles.size > 1) {
|
||||
GroupItem(
|
||||
onClick = { showBundleSelector = true },
|
||||
headline = {
|
||||
Text(
|
||||
stringResource(R.string.patch_options_reset_bundle),
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
},
|
||||
description = R.string.patch_options_reset_bundle_description,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun PackageSelector(packages: Set<String>, onFinish: (String?) -> Unit) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val noPackages = packages.isEmpty()
|
||||
|
||||
LaunchedEffect(noPackages) {
|
||||
if (noPackages) {
|
||||
context.toast("No packages available.")
|
||||
onFinish(null)
|
||||
}
|
||||
}
|
||||
|
||||
if (noPackages) return
|
||||
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = { onFinish(null) }
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier
|
||||
.height(48.dp)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
text = "Select package",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
packages.forEach {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier
|
||||
.height(48.dp)
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
onFinish(it)
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun GroupItem(
|
||||
onClick: () -> Unit,
|
||||
@StringRes headline: Int,
|
||||
@StringRes description: Int? = null
|
||||
) {
|
||||
SettingsListItem(
|
||||
modifier = Modifier.clickable { onClick() },
|
||||
headlineContent = stringResource(headline),
|
||||
supportingContent = description?.let { stringResource(it) }
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun GroupItem(
|
||||
onClick: () -> Unit,
|
||||
headline: @Composable () -> Unit,
|
||||
@StringRes description: Int? = null
|
||||
) {
|
||||
SettingsListItem(
|
||||
modifier = Modifier.clickable { onClick() },
|
||||
headlineContent = headline,
|
||||
supportingContent = description?.let { stringResource(it) }
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun KeystoreCredentialsDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
onSubmit: (String, String) -> Unit
|
||||
) {
|
||||
var cn by rememberSaveable { mutableStateOf("") }
|
||||
var pass by rememberSaveable { mutableStateOf("") }
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
onSubmit(cn, pass)
|
||||
}
|
||||
) {
|
||||
Text(stringResource(R.string.restore_keystore_dialog_button))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(stringResource(R.string.cancel))
|
||||
}
|
||||
},
|
||||
icon = {
|
||||
Icon(Icons.Outlined.Key, null)
|
||||
},
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.restore_keystore_dialog_title),
|
||||
style = MaterialTheme.typography.headlineSmall.copy(textAlign = TextAlign.Center),
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.restore_keystore_dialog_description),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = cn,
|
||||
onValueChange = { cn = it },
|
||||
label = { Text(stringResource(R.string.restore_keystore_dialog_alias_field)) }
|
||||
)
|
||||
PasswordField(
|
||||
value = pass,
|
||||
onValueChange = { pass = it },
|
||||
label = { Text(stringResource(R.string.restore_keystore_dialog_password_field)) }
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -51,7 +51,7 @@ import org.koin.androidx.compose.koinViewModel
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ContributorSettingsScreen(
|
||||
fun ContributorScreen(
|
||||
onBackClick: () -> Unit,
|
||||
viewModel: ContributorViewModel = koinViewModel()
|
||||
) {
|
||||
@@ -97,14 +97,7 @@ fun ContributorSettingsScreen(
|
||||
)
|
||||
}
|
||||
}
|
||||
} ?: item {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
LoadingIndicator()
|
||||
}
|
||||
}
|
||||
} ?: item { LoadingIndicator() }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,23 +12,19 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.domain.manager.PreferencesManager
|
||||
import app.revanced.manager.ui.component.AppTopBar
|
||||
import app.revanced.manager.ui.component.GroupHeader
|
||||
import app.revanced.manager.ui.component.settings.BooleanItem
|
||||
import app.revanced.manager.ui.component.settings.SettingsListItem
|
||||
import app.revanced.manager.ui.viewmodel.DeveloperOptionsViewModel
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
import org.koin.compose.koinInject
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun DeveloperSettingsScreen(
|
||||
fun DeveloperOptionsScreen(
|
||||
onBackClick: () -> Unit,
|
||||
vm: DeveloperOptionsViewModel = koinViewModel()
|
||||
) {
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
val prefs: PreferencesManager = koinInject()
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
@@ -41,21 +37,20 @@ fun DeveloperSettingsScreen(
|
||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
) { paddingValues ->
|
||||
Column(modifier = Modifier.padding(paddingValues)) {
|
||||
GroupHeader(stringResource(R.string.manager))
|
||||
BooleanItem(
|
||||
preference = prefs.showDeveloperSettings,
|
||||
headline = R.string.developer_options,
|
||||
description = R.string.developer_options_description,
|
||||
GroupHeader(stringResource(R.string.patch_bundles_section))
|
||||
SettingsListItem(
|
||||
headlineContent = stringResource(R.string.patch_bundles_force_download),
|
||||
modifier = Modifier.clickable(onClick = vm::redownloadBundles)
|
||||
)
|
||||
SettingsListItem(
|
||||
headlineContent = stringResource(R.string.patch_bundles_reset),
|
||||
modifier = Modifier.clickable(onClick = vm::resetBundles)
|
||||
)
|
||||
|
||||
GroupHeader(stringResource(R.string.patches))
|
||||
GroupHeader(stringResource(R.string.testing))
|
||||
SettingsListItem(
|
||||
headlineContent = stringResource(R.string.patches_force_download),
|
||||
modifier = Modifier.clickable(onClick = vm::redownloadBundles)
|
||||
)
|
||||
SettingsListItem(
|
||||
headlineContent = stringResource(R.string.patches_reset),
|
||||
modifier = Modifier.clickable(onClick = vm::redownloadBundles)
|
||||
headlineContent = stringResource(R.string.disable_safeguard),
|
||||
modifier = Modifier.clickable(onClick = vm::disableSafeguard)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -2,13 +2,13 @@ package app.revanced.manager.ui.screen.settings
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.outlined.Delete
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
@@ -18,7 +18,9 @@ import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults
|
||||
import androidx.compose.material3.pulltorefresh.pullToRefresh
|
||||
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -26,11 +28,13 @@ 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
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.zIndex
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.network.downloader.DownloaderPluginState
|
||||
@@ -39,7 +43,6 @@ import app.revanced.manager.ui.component.AppTopBar
|
||||
import app.revanced.manager.ui.component.ExceptionViewerDialog
|
||||
import app.revanced.manager.ui.component.GroupHeader
|
||||
import app.revanced.manager.ui.component.LazyColumnWithScrollbar
|
||||
import app.revanced.manager.ui.component.ConfirmDialog
|
||||
import app.revanced.manager.ui.component.haptics.HapticCheckbox
|
||||
import app.revanced.manager.ui.component.settings.SettingsListItem
|
||||
import app.revanced.manager.ui.viewmodel.DownloadsViewModel
|
||||
@@ -52,30 +55,20 @@ fun DownloadsSettingsScreen(
|
||||
onBackClick: () -> Unit,
|
||||
viewModel: DownloadsViewModel = koinViewModel()
|
||||
) {
|
||||
val pullRefreshState = rememberPullToRefreshState()
|
||||
val downloadedApps by viewModel.downloadedApps.collectAsStateWithLifecycle(emptyList())
|
||||
val pluginStates by viewModel.downloaderPluginStates.collectAsStateWithLifecycle()
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
var showDeleteConfirmationDialog by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
if (showDeleteConfirmationDialog) {
|
||||
ConfirmDialog(
|
||||
onDismiss = { showDeleteConfirmationDialog = false },
|
||||
onConfirm = { viewModel.deleteApps() },
|
||||
title = stringResource(R.string.downloader_plugin_delete_apps_title),
|
||||
description = stringResource(R.string.downloader_plugin_delete_apps_description),
|
||||
icon = Icons.Outlined.Delete
|
||||
)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
AppTopBar(
|
||||
title = stringResource(R.string.downloads),
|
||||
title = stringResource(R.string.extensions),
|
||||
scrollBehavior = scrollBehavior,
|
||||
onBackClick = onBackClick,
|
||||
actions = {
|
||||
if (viewModel.appSelection.isNotEmpty()) {
|
||||
IconButton(onClick = { showDeleteConfirmationDialog = true }) {
|
||||
IconButton(onClick = { viewModel.deleteApps() }) {
|
||||
Icon(Icons.Default.Delete, stringResource(R.string.delete))
|
||||
}
|
||||
}
|
||||
@@ -84,138 +77,152 @@ fun DownloadsSettingsScreen(
|
||||
},
|
||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
) { paddingValues ->
|
||||
PullToRefreshBox(
|
||||
onRefresh = viewModel::refreshPlugins,
|
||||
isRefreshing = viewModel.isRefreshingPlugins,
|
||||
modifier = Modifier.padding(paddingValues)
|
||||
Box(
|
||||
contentAlignment = Alignment.TopCenter,
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.fillMaxWidth()
|
||||
.zIndex(1f)
|
||||
) {
|
||||
LazyColumnWithScrollbar(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
item {
|
||||
GroupHeader(stringResource(R.string.downloader_plugins))
|
||||
}
|
||||
pluginStates.forEach { (packageName, state) ->
|
||||
item(key = packageName) {
|
||||
var showDialog by rememberSaveable {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
PullToRefreshDefaults.Indicator(
|
||||
state = pullRefreshState,
|
||||
isRefreshing = viewModel.isRefreshingPlugins
|
||||
)
|
||||
}
|
||||
|
||||
fun dismiss() {
|
||||
showDialog = false
|
||||
}
|
||||
LazyColumnWithScrollbar(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.pullToRefresh(
|
||||
isRefreshing = viewModel.isRefreshingPlugins,
|
||||
state = pullRefreshState,
|
||||
onRefresh = viewModel::refreshPlugins
|
||||
)
|
||||
) {
|
||||
item {
|
||||
GroupHeader(stringResource(R.string.downloader_plugins))
|
||||
}
|
||||
pluginStates.forEach { (packageName, state) ->
|
||||
item(key = packageName) {
|
||||
var showDialog by rememberSaveable {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
val packageInfo =
|
||||
fun dismiss() {
|
||||
showDialog = false
|
||||
}
|
||||
|
||||
val packageInfo =
|
||||
remember(packageName) {
|
||||
viewModel.pm.getPackageInfo(
|
||||
packageName
|
||||
)
|
||||
} ?: return@item
|
||||
|
||||
if (showDialog) {
|
||||
val signature =
|
||||
remember(packageName) {
|
||||
viewModel.pm.getPackageInfo(
|
||||
packageName
|
||||
)
|
||||
} ?: return@item
|
||||
|
||||
if (showDialog) {
|
||||
val signature =
|
||||
remember(packageName) {
|
||||
val androidSignature =
|
||||
viewModel.pm.getSignature(packageName)
|
||||
val hash = MessageDigest.getInstance("SHA-256")
|
||||
.digest(androidSignature.toByteArray())
|
||||
hash.toHexString(format = HexFormat.UpperCase)
|
||||
}
|
||||
|
||||
when (state) {
|
||||
is DownloaderPluginState.Loaded -> TrustDialog(
|
||||
title = R.string.downloader_plugin_revoke_trust_dialog_title,
|
||||
body = stringResource(
|
||||
R.string.downloader_plugin_trust_dialog_body,
|
||||
packageName,
|
||||
signature
|
||||
),
|
||||
onDismiss = ::dismiss,
|
||||
onConfirm = {
|
||||
viewModel.revokePluginTrust(packageName)
|
||||
dismiss()
|
||||
}
|
||||
)
|
||||
|
||||
is DownloaderPluginState.Failed -> ExceptionViewerDialog(
|
||||
text = remember(state.throwable) {
|
||||
state.throwable.stackTraceToString()
|
||||
},
|
||||
onDismiss = ::dismiss
|
||||
)
|
||||
|
||||
is DownloaderPluginState.Untrusted -> TrustDialog(
|
||||
title = R.string.downloader_plugin_trust_dialog_title,
|
||||
body = stringResource(
|
||||
R.string.downloader_plugin_trust_dialog_body,
|
||||
packageName,
|
||||
signature
|
||||
),
|
||||
onDismiss = ::dismiss,
|
||||
onConfirm = {
|
||||
viewModel.trustPlugin(packageName)
|
||||
dismiss()
|
||||
}
|
||||
)
|
||||
val androidSignature =
|
||||
viewModel.pm.getSignature(packageName)
|
||||
val hash = MessageDigest.getInstance("SHA-256")
|
||||
.digest(androidSignature.toByteArray())
|
||||
hash.toHexString(format = HexFormat.UpperCase)
|
||||
}
|
||||
}
|
||||
|
||||
SettingsListItem(
|
||||
modifier = Modifier.clickable { showDialog = true },
|
||||
headlineContent = {
|
||||
AppLabel(
|
||||
packageInfo = packageInfo,
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
},
|
||||
supportingContent = stringResource(
|
||||
when (state) {
|
||||
is DownloaderPluginState.Loaded -> R.string.downloader_plugin_state_trusted
|
||||
is DownloaderPluginState.Failed -> R.string.downloader_plugin_state_failed
|
||||
is DownloaderPluginState.Untrusted -> R.string.downloader_plugin_state_untrusted
|
||||
when (state) {
|
||||
is DownloaderPluginState.Loaded -> TrustDialog(
|
||||
title = R.string.downloader_plugin_revoke_trust_dialog_title,
|
||||
body = stringResource(
|
||||
R.string.downloader_plugin_trust_dialog_body,
|
||||
packageName,
|
||||
signature
|
||||
),
|
||||
onDismiss = ::dismiss,
|
||||
onConfirm = {
|
||||
viewModel.revokePluginTrust(packageName)
|
||||
dismiss()
|
||||
}
|
||||
),
|
||||
trailingContent = { Text(packageInfo.versionName!!) }
|
||||
)
|
||||
}
|
||||
}
|
||||
if (pluginStates.isEmpty()) {
|
||||
item {
|
||||
Text(
|
||||
stringResource(R.string.downloader_no_plugins_installed),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
item {
|
||||
GroupHeader(stringResource(R.string.downloaded_apps))
|
||||
}
|
||||
items(downloadedApps, key = { it.packageName to it.version }) { app ->
|
||||
val selected = app in viewModel.appSelection
|
||||
is DownloaderPluginState.Failed -> ExceptionViewerDialog(
|
||||
text = remember(state.throwable) {
|
||||
state.throwable.stackTraceToString()
|
||||
},
|
||||
onDismiss = ::dismiss
|
||||
)
|
||||
|
||||
is DownloaderPluginState.Untrusted -> TrustDialog(
|
||||
title = R.string.downloader_plugin_trust_dialog_title,
|
||||
body = stringResource(
|
||||
R.string.downloader_plugin_trust_dialog_body,
|
||||
packageName,
|
||||
signature
|
||||
),
|
||||
onDismiss = ::dismiss,
|
||||
onConfirm = {
|
||||
viewModel.trustPlugin(packageName)
|
||||
dismiss()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
SettingsListItem(
|
||||
modifier = Modifier.clickable { viewModel.toggleApp(app) },
|
||||
headlineContent = app.packageName,
|
||||
leadingContent = (@Composable {
|
||||
HapticCheckbox(
|
||||
checked = selected,
|
||||
onCheckedChange = { viewModel.toggleApp(app) }
|
||||
modifier = Modifier.clickable { showDialog = true },
|
||||
headlineContent = {
|
||||
AppLabel(
|
||||
packageInfo = packageInfo,
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
}).takeIf { viewModel.appSelection.isNotEmpty() },
|
||||
supportingContent = app.version,
|
||||
tonalElevation = if (selected) 8.dp else 0.dp
|
||||
},
|
||||
supportingContent = stringResource(
|
||||
when (state) {
|
||||
is DownloaderPluginState.Loaded -> R.string.downloader_plugin_state_trusted
|
||||
is DownloaderPluginState.Failed -> R.string.downloader_plugin_state_failed
|
||||
is DownloaderPluginState.Untrusted -> R.string.downloader_plugin_state_untrusted
|
||||
}
|
||||
),
|
||||
trailingContent = { Text(packageInfo.versionName!!) }
|
||||
)
|
||||
}
|
||||
if (downloadedApps.isEmpty()) {
|
||||
item {
|
||||
Text(
|
||||
stringResource(R.string.downloader_settings_no_apps),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
textAlign = TextAlign.Center
|
||||
}
|
||||
if (pluginStates.isEmpty()) {
|
||||
item {
|
||||
Text(
|
||||
stringResource(R.string.downloader_no_plugins_installed),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
GroupHeader(stringResource(R.string.downloaded_apps))
|
||||
}
|
||||
items(downloadedApps, key = { it.packageName to it.version }) { app ->
|
||||
val selected = app in viewModel.appSelection
|
||||
|
||||
SettingsListItem(
|
||||
modifier = Modifier.clickable { viewModel.toggleApp(app) },
|
||||
headlineContent = app.packageName,
|
||||
leadingContent = (@Composable {
|
||||
HapticCheckbox(
|
||||
checked = selected,
|
||||
onCheckedChange = { viewModel.toggleApp(app) }
|
||||
)
|
||||
}
|
||||
}).takeIf { viewModel.appSelection.isNotEmpty() },
|
||||
supportingContent = app.version,
|
||||
tonalElevation = if (selected) 8.dp else 0.dp
|
||||
)
|
||||
}
|
||||
if (downloadedApps.isEmpty()) {
|
||||
item {
|
||||
Text(
|
||||
stringResource(R.string.downloader_settings_no_apps),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
@@ -42,6 +43,7 @@ import org.koin.compose.koinInject
|
||||
@Composable
|
||||
fun GeneralSettingsScreen(
|
||||
onBackClick: () -> Unit,
|
||||
onUpdateClick: () -> Unit,
|
||||
viewModel: GeneralSettingsViewModel = koinViewModel()
|
||||
) {
|
||||
val prefs = viewModel.prefs
|
||||
@@ -76,10 +78,10 @@ fun GeneralSettingsScreen(
|
||||
val theme by prefs.theme.getAsState()
|
||||
SettingsListItem(
|
||||
modifier = Modifier.clickable { showThemePicker = true },
|
||||
headlineContent = stringResource(R.string.theme),
|
||||
supportingContent = stringResource(R.string.theme_description),
|
||||
headlineContent = stringResource(R.string.theme_mode),
|
||||
supportingContent = stringResource(R.string.theme_mode_description),
|
||||
trailingContent = {
|
||||
FilledTonalButton(
|
||||
Button(
|
||||
onClick = {
|
||||
showThemePicker = true
|
||||
}
|
||||
@@ -92,11 +94,24 @@ fun GeneralSettingsScreen(
|
||||
BooleanItem(
|
||||
preference = prefs.dynamicColor,
|
||||
coroutineScope = coroutineScope,
|
||||
headline = R.string.dynamic_color,
|
||||
description = R.string.dynamic_color_description
|
||||
headline = R.string.personalized_color,
|
||||
description = R.string.personalized_color_description
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
GroupHeader(stringResource(R.string.update))
|
||||
BooleanItem(
|
||||
preference = prefs.managerAutoUpdates,
|
||||
headline = R.string.check_for_update,
|
||||
description = R.string.check_for_update_auto_description
|
||||
)
|
||||
FilledTonalButton (
|
||||
modifier = Modifier.padding(top = paddingValues.calculateTopPadding()),
|
||||
onClick = onUpdateClick
|
||||
) {
|
||||
Text(stringResource(R.string.check_for_update))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,7 +125,7 @@ private fun ThemePicker(
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(stringResource(R.string.theme)) },
|
||||
title = { Text(stringResource(R.string.theme_mode)) },
|
||||
text = {
|
||||
Column {
|
||||
Theme.entries.forEach {
|
||||
|
||||
@@ -1,437 +0,0 @@
|
||||
package app.revanced.manager.ui.screen.settings
|
||||
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Key
|
||||
import androidx.compose.material.icons.outlined.WarningAmber
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.ui.component.AppTopBar
|
||||
import app.revanced.manager.ui.component.ColumnWithScrollbar
|
||||
import app.revanced.manager.ui.component.ConfirmDialog
|
||||
import app.revanced.manager.ui.component.GroupHeader
|
||||
import app.revanced.manager.ui.component.PasswordField
|
||||
import app.revanced.manager.ui.component.bundle.BundleSelector
|
||||
import app.revanced.manager.ui.component.settings.ExpandableSettingListItem
|
||||
import app.revanced.manager.ui.component.settings.SettingsListItem
|
||||
import app.revanced.manager.ui.viewmodel.ImportExportViewModel
|
||||
import app.revanced.manager.ui.viewmodel.ResetDialogState
|
||||
import app.revanced.manager.util.toast
|
||||
import app.revanced.manager.util.uiSafe
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ImportExportSettingsScreen(
|
||||
onBackClick: () -> Unit,
|
||||
vm: ImportExportViewModel = koinViewModel()
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
var selectorDialog by rememberSaveable { mutableStateOf<(@Composable () -> Unit)?>(null) }
|
||||
|
||||
val importKeystoreLauncher =
|
||||
rememberLauncherForActivityResult(contract = ActivityResultContracts.GetContent()) {
|
||||
it?.let { uri -> vm.startKeystoreImport(uri) }
|
||||
}
|
||||
val exportKeystoreLauncher =
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("*/*")) {
|
||||
it?.let(vm::exportKeystore)
|
||||
}
|
||||
|
||||
val patchBundles by vm.patchBundles.collectAsStateWithLifecycle(initialValue = emptyList())
|
||||
val packagesWithSelections by vm.packagesWithSelection.collectAsStateWithLifecycle(initialValue = emptySet())
|
||||
val packagesWithOptions by vm.packagesWithOptions.collectAsStateWithLifecycle(initialValue = emptySet())
|
||||
|
||||
vm.selectionAction?.let { action ->
|
||||
val launcher = rememberLauncherForActivityResult(action.activityContract) { uri ->
|
||||
if (uri == null) {
|
||||
vm.clearSelectionAction()
|
||||
} else {
|
||||
vm.executeSelectionAction(uri)
|
||||
}
|
||||
}
|
||||
|
||||
if (vm.selectedBundle == null) {
|
||||
BundleSelector(patchBundles) {
|
||||
if (it == null) {
|
||||
vm.clearSelectionAction()
|
||||
} else {
|
||||
vm.selectBundle(it)
|
||||
launcher.launch(action.activityArg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (vm.showCredentialsDialog) {
|
||||
KeystoreCredentialsDialog(
|
||||
onDismissRequest = vm::cancelKeystoreImport,
|
||||
onSubmit = { cn, pass ->
|
||||
vm.viewModelScope.launch {
|
||||
uiSafe(context, R.string.failed_to_import_keystore, "Failed to import keystore") {
|
||||
val result = vm.tryKeystoreImport(cn, pass)
|
||||
if (!result) context.toast(context.getString(R.string.import_keystore_wrong_credentials))
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
vm.resetDialogState?.let {
|
||||
with(vm.resetDialogState!!) {
|
||||
ConfirmDialog(
|
||||
onDismiss = { vm.resetDialogState = null },
|
||||
onConfirm = onConfirm,
|
||||
title = stringResource(titleResId),
|
||||
description = dialogOptionName?.let {
|
||||
stringResource(descriptionResId, it)
|
||||
} ?: stringResource(descriptionResId),
|
||||
icon = Icons.Outlined.WarningAmber
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
AppTopBar(
|
||||
title = stringResource(R.string.import_export),
|
||||
scrollBehavior = scrollBehavior,
|
||||
onBackClick = onBackClick
|
||||
)
|
||||
},
|
||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
) { paddingValues ->
|
||||
ColumnWithScrollbar(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
) {
|
||||
selectorDialog?.invoke()
|
||||
|
||||
GroupHeader(stringResource(R.string.import_))
|
||||
GroupItem(
|
||||
onClick = {
|
||||
importKeystoreLauncher.launch("*/*")
|
||||
},
|
||||
headline = R.string.import_keystore,
|
||||
description = R.string.import_keystore_description
|
||||
)
|
||||
GroupItem(
|
||||
onClick = vm::importSelection,
|
||||
headline = R.string.import_patch_selection,
|
||||
description = R.string.import_patch_selection_description
|
||||
)
|
||||
|
||||
GroupHeader(stringResource(R.string.export))
|
||||
GroupItem(
|
||||
onClick = {
|
||||
if (!vm.canExport()) {
|
||||
context.toast(context.getString(R.string.export_keystore_unavailable))
|
||||
return@GroupItem
|
||||
}
|
||||
exportKeystoreLauncher.launch("Manager.keystore")
|
||||
},
|
||||
headline = R.string.export_keystore,
|
||||
description = R.string.export_keystore_description
|
||||
)
|
||||
GroupItem(
|
||||
onClick = vm::exportSelection,
|
||||
headline = R.string.export_patch_selection,
|
||||
description = R.string.export_patch_selection_description
|
||||
)
|
||||
|
||||
GroupHeader(stringResource(R.string.reset))
|
||||
GroupItem(
|
||||
onClick = {
|
||||
vm.resetDialogState = ResetDialogState.Keystore {
|
||||
vm.regenerateKeystore()
|
||||
}
|
||||
},
|
||||
headline = R.string.regenerate_keystore,
|
||||
description = R.string.regenerate_keystore_description
|
||||
)
|
||||
|
||||
ExpandableSettingListItem(
|
||||
headlineContent = stringResource(R.string.reset_patch_selection),
|
||||
supportingContent = stringResource(R.string.reset_patch_selection_description),
|
||||
expandableContent = {
|
||||
GroupItem(
|
||||
onClick = {
|
||||
vm.resetDialogState = ResetDialogState.PatchSelectionAll {
|
||||
vm.resetSelection()
|
||||
}
|
||||
},
|
||||
headline = R.string.patch_selection_reset_all,
|
||||
description = R.string.patch_selection_reset_all_description
|
||||
)
|
||||
|
||||
GroupItem(
|
||||
onClick = {
|
||||
selectorDialog = {
|
||||
PackageSelector(packages = packagesWithSelections) { packageName ->
|
||||
packageName?.also {
|
||||
vm.resetDialogState =
|
||||
ResetDialogState.PatchSelectionPackage(packageName) {
|
||||
vm.resetSelectionForPackage(packageName)
|
||||
}
|
||||
}
|
||||
selectorDialog = null
|
||||
}
|
||||
}
|
||||
},
|
||||
headline = R.string.patch_selection_reset_package,
|
||||
description = R.string.patch_selection_reset_package_description
|
||||
)
|
||||
|
||||
if (patchBundles.isNotEmpty()) {
|
||||
GroupItem(
|
||||
onClick = {
|
||||
selectorDialog = {
|
||||
BundleSelector(sources = patchBundles) { src ->
|
||||
src?.also {
|
||||
coroutineScope.launch {
|
||||
vm.resetDialogState =
|
||||
ResetDialogState.PatchSelectionBundle(it.name) {
|
||||
vm.resetSelectionForPatchBundle(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
selectorDialog = null
|
||||
}
|
||||
}
|
||||
},
|
||||
headline = R.string.patch_selection_reset_patches,
|
||||
description = R.string.patch_selection_reset_patches_description
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
ExpandableSettingListItem(
|
||||
headlineContent = stringResource(R.string.reset_patch_options),
|
||||
supportingContent = stringResource(R.string.reset_patch_options_description),
|
||||
expandableContent = {
|
||||
GroupItem(
|
||||
onClick = {
|
||||
vm.resetDialogState = ResetDialogState.PatchOptionsAll {
|
||||
vm.resetOptions()
|
||||
}
|
||||
}, // TODO: patch options import/export.
|
||||
headline = R.string.patch_options_reset_all,
|
||||
description = R.string.patch_options_reset_all_description,
|
||||
)
|
||||
|
||||
GroupItem(
|
||||
onClick = {
|
||||
selectorDialog = {
|
||||
PackageSelector(packages = packagesWithOptions) { packageName ->
|
||||
packageName?.also {
|
||||
vm.resetDialogState =
|
||||
ResetDialogState.PatchOptionPackage(packageName) {
|
||||
vm.resetOptionsForPackage(packageName)
|
||||
}
|
||||
}
|
||||
selectorDialog = null
|
||||
}
|
||||
}
|
||||
},
|
||||
headline = R.string.patch_options_reset_package,
|
||||
description = R.string.patch_options_reset_package_description
|
||||
)
|
||||
|
||||
if (patchBundles.isNotEmpty()) {
|
||||
GroupItem(
|
||||
onClick = {
|
||||
selectorDialog = {
|
||||
BundleSelector(sources = patchBundles) { src ->
|
||||
src?.also {
|
||||
coroutineScope.launch {
|
||||
vm.resetDialogState =
|
||||
ResetDialogState.PatchOptionBundle(src.name) {
|
||||
vm.resetOptionsForBundle(src)
|
||||
}
|
||||
}
|
||||
}
|
||||
selectorDialog = null
|
||||
}
|
||||
}
|
||||
},
|
||||
headline = R.string.patch_options_reset_patches,
|
||||
description = R.string.patch_options_reset_patches_description,
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun PackageSelector(packages: Set<String>, onFinish: (String?) -> Unit) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val noPackages = packages.isEmpty()
|
||||
|
||||
LaunchedEffect(noPackages) {
|
||||
if (noPackages) {
|
||||
context.toast("No packages available.")
|
||||
onFinish(null)
|
||||
}
|
||||
}
|
||||
|
||||
if (noPackages) return
|
||||
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = { onFinish(null) }
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier
|
||||
.height(48.dp)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
text = "Select package",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
packages.forEach {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier
|
||||
.height(48.dp)
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
onFinish(it)
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun GroupItem(
|
||||
onClick: () -> Unit,
|
||||
@StringRes headline: Int,
|
||||
@StringRes description: Int? = null
|
||||
) {
|
||||
SettingsListItem(
|
||||
modifier = Modifier.clickable { onClick() },
|
||||
headlineContent = stringResource(headline),
|
||||
supportingContent = description?.let { stringResource(it) }
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun KeystoreCredentialsDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
onSubmit: (String, String) -> Unit
|
||||
) {
|
||||
var cn by rememberSaveable { mutableStateOf("") }
|
||||
var pass by rememberSaveable { mutableStateOf("") }
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
onSubmit(cn, pass)
|
||||
}
|
||||
) {
|
||||
Text(stringResource(R.string.import_keystore_dialog_button))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(stringResource(R.string.cancel))
|
||||
}
|
||||
},
|
||||
icon = {
|
||||
Icon(Icons.Outlined.Key, null)
|
||||
},
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.import_keystore_dialog_title),
|
||||
style = MaterialTheme.typography.headlineSmall.copy(textAlign = TextAlign.Center),
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.import_keystore_dialog_description),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = cn,
|
||||
onValueChange = { cn = it },
|
||||
label = { Text(stringResource(R.string.import_keystore_dialog_alias_field)) }
|
||||
)
|
||||
PasswordField(
|
||||
value = pass,
|
||||
onValueChange = { pass = it },
|
||||
label = { Text(stringResource(R.string.import_keystore_dialog_password_field)) }
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -15,11 +15,10 @@ import app.revanced.manager.ui.component.AppTopBar
|
||||
import app.revanced.manager.ui.component.Scrollbar
|
||||
import com.mikepenz.aboutlibraries.ui.compose.LibrariesContainer
|
||||
import com.mikepenz.aboutlibraries.ui.compose.LibraryDefaults
|
||||
import com.mikepenz.aboutlibraries.ui.compose.libraryColors
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun LicensesSettingsScreen(
|
||||
fun LicensesScreen(
|
||||
onBackClick: () -> Unit,
|
||||
) {
|
||||
AppScaffold(
|
||||
@@ -27,7 +27,7 @@ import org.koin.androidx.compose.koinViewModel
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ChangelogsSettingsScreen(
|
||||
fun ChangelogsScreen(
|
||||
onBackClick: () -> Unit,
|
||||
vm: ChangelogsViewModel = koinViewModel()
|
||||
) {
|
||||
@@ -1,97 +0,0 @@
|
||||
package app.revanced.manager.ui.screen.settings.update
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.ui.component.AppTopBar
|
||||
import app.revanced.manager.ui.component.ColumnWithScrollbar
|
||||
import app.revanced.manager.ui.component.GroupHeader
|
||||
import app.revanced.manager.ui.component.settings.BooleanItem
|
||||
import app.revanced.manager.ui.component.settings.SettingsListItem
|
||||
import app.revanced.manager.ui.viewmodel.UpdatesSettingsViewModel
|
||||
import app.revanced.manager.util.toast
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun UpdatesSettingsScreen(
|
||||
onBackClick: () -> Unit,
|
||||
onChangelogClick: () -> Unit,
|
||||
onUpdateClick: () -> Unit,
|
||||
vm: UpdatesSettingsViewModel = koinViewModel(),
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
AppTopBar(
|
||||
title = stringResource(R.string.updates),
|
||||
scrollBehavior = scrollBehavior,
|
||||
onBackClick = onBackClick
|
||||
)
|
||||
},
|
||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
) { paddingValues ->
|
||||
ColumnWithScrollbar(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
) {
|
||||
GroupHeader(stringResource(R.string.manager))
|
||||
|
||||
SettingsListItem(
|
||||
modifier = Modifier.clickable {
|
||||
coroutineScope.launch {
|
||||
if (!vm.isConnected) {
|
||||
context.toast(context.getString(R.string.no_network_toast))
|
||||
return@launch
|
||||
}
|
||||
if (vm.checkForUpdates()) onUpdateClick()
|
||||
}
|
||||
},
|
||||
headlineContent = stringResource(R.string.manual_update_check),
|
||||
supportingContent = stringResource(R.string.manual_update_check_description)
|
||||
)
|
||||
|
||||
SettingsListItem(
|
||||
modifier = Modifier.clickable {
|
||||
if (!vm.isConnected) {
|
||||
context.toast(context.getString(R.string.no_network_toast))
|
||||
return@clickable
|
||||
}
|
||||
onChangelogClick()
|
||||
},
|
||||
headlineContent = stringResource(R.string.changelog),
|
||||
supportingContent = stringResource(
|
||||
R.string.changelog_description
|
||||
)
|
||||
)
|
||||
|
||||
BooleanItem(
|
||||
preference = vm.managerAutoUpdates,
|
||||
headline = R.string.update_checking_manager,
|
||||
description = R.string.update_checking_manager_description
|
||||
)
|
||||
|
||||
BooleanItem(
|
||||
preference = vm.showManagerUpdateDialogOnLaunch,
|
||||
headline = R.string.show_manager_update_dialog_on_launch,
|
||||
description = R.string.show_manager_update_dialog_on_launch_description
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,6 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.revanced.manager.data.platform.NetworkInfo
|
||||
import app.revanced.manager.domain.manager.PreferencesManager
|
||||
import app.revanced.manager.network.api.ReVancedAPI
|
||||
import app.revanced.manager.network.dto.ReVancedDonationLink
|
||||
import app.revanced.manager.network.dto.ReVancedSocial
|
||||
@@ -25,27 +23,16 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class AboutViewModel(
|
||||
private val reVancedAPI: ReVancedAPI,
|
||||
private val network: NetworkInfo,
|
||||
prefs: PreferencesManager,
|
||||
) : ViewModel() {
|
||||
class AboutViewModel(private val reVancedAPI: ReVancedAPI) : ViewModel() {
|
||||
var socials by mutableStateOf(emptyList<ReVancedSocial>())
|
||||
private set
|
||||
private set
|
||||
var contact by mutableStateOf<String?>(null)
|
||||
private set
|
||||
private set
|
||||
var donate by mutableStateOf<String?>(null)
|
||||
private set
|
||||
val isConnected: Boolean
|
||||
get() = network.isConnected()
|
||||
|
||||
val showDeveloperSettings = prefs.showDeveloperSettings
|
||||
private set
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
if (!isConnected) {
|
||||
return@launch
|
||||
}
|
||||
withContext(Dispatchers.IO) {
|
||||
reVancedAPI.getInfo("https://api.revanced.app").getOrNull()
|
||||
}?.let {
|
||||
@@ -57,8 +44,6 @@ class AboutViewModel(
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val DEVELOPER_OPTIONS_TAPS = 5
|
||||
|
||||
private val socialIcons = mapOf(
|
||||
"Discord" to FontAwesomeIcons.Brands.Discord,
|
||||
"GitHub" to FontAwesomeIcons.Brands.Github,
|
||||
|
||||
@@ -34,7 +34,7 @@ class AdvancedSettingsViewModel(
|
||||
return "revanced-manager_logcat_$time"
|
||||
}
|
||||
|
||||
fun setApiUrl(value: String) = viewModelScope.launch(Dispatchers.Default) {
|
||||
fun setApiSource(value: String) = viewModelScope.launch(Dispatchers.Default) {
|
||||
if (value == prefs.api.get()) return@launch
|
||||
|
||||
prefs.api.update(value)
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
package app.revanced.manager.ui.viewmodel
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.revanced.manager.domain.bundles.PatchBundleSource
|
||||
import app.revanced.manager.domain.bundles.RemotePatchBundle
|
||||
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||
import app.revanced.manager.util.mutableStateSetOf
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.get
|
||||
|
||||
class BundleListViewModel : ViewModel(), KoinComponent {
|
||||
private val patchBundleRepository: PatchBundleRepository = get()
|
||||
val patchCounts = patchBundleRepository.patchCountsFlow
|
||||
var isRefreshing by mutableStateOf(false)
|
||||
private set
|
||||
|
||||
val sources = combine(
|
||||
patchBundleRepository.sources,
|
||||
patchBundleRepository.patchCountsFlow
|
||||
) { sources, patchCounts ->
|
||||
isRefreshing = false
|
||||
sources.sortedByDescending { patchCounts[it.uid] ?: 0 }
|
||||
}
|
||||
|
||||
val selectedSources = mutableStateSetOf<Int>()
|
||||
|
||||
fun refresh() = viewModelScope.launch {
|
||||
isRefreshing = true
|
||||
patchBundleRepository.reload()
|
||||
}
|
||||
|
||||
private suspend fun getSelectedSources() = patchBundleRepository.sources
|
||||
.first()
|
||||
.filter { it.uid in selectedSources }
|
||||
.also {
|
||||
selectedSources.clear()
|
||||
}
|
||||
|
||||
fun handleEvent(event: Event) {
|
||||
when (event) {
|
||||
Event.CANCEL -> selectedSources.clear()
|
||||
Event.DELETE_SELECTED -> viewModelScope.launch {
|
||||
patchBundleRepository.remove(*getSelectedSources().toTypedArray())
|
||||
}
|
||||
|
||||
Event.UPDATE_SELECTED -> viewModelScope.launch {
|
||||
patchBundleRepository.update(
|
||||
*getSelectedSources().filterIsInstance<RemotePatchBundle>().toTypedArray(),
|
||||
showToast = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun delete(src: PatchBundleSource) =
|
||||
viewModelScope.launch { patchBundleRepository.remove(src) }
|
||||
|
||||
fun update(src: PatchBundleSource) = viewModelScope.launch {
|
||||
if (src !is RemotePatchBundle) return@launch
|
||||
|
||||
patchBundleRepository.update(src, showToast = true)
|
||||
}
|
||||
|
||||
enum class Event {
|
||||
DELETE_SELECTED,
|
||||
UPDATE_SELECTED,
|
||||
CANCEL,
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
package app.revanced.manager.ui.viewmodel
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Application
|
||||
import android.content.ContentResolver
|
||||
import android.net.Uri
|
||||
@@ -25,10 +24,10 @@ import app.revanced.manager.network.api.ReVancedAPI
|
||||
import app.revanced.manager.util.PM
|
||||
import app.revanced.manager.util.toast
|
||||
import app.revanced.manager.util.uiSafe
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class DashboardViewModel(
|
||||
@@ -41,12 +40,13 @@ class DashboardViewModel(
|
||||
private val pm: PM,
|
||||
) : ViewModel() {
|
||||
val availablePatches =
|
||||
patchBundleRepository.bundleInfoFlow.map { it.values.sumOf { bundle -> bundle.patches.size } }
|
||||
patchBundleRepository.bundles.map { it.values.sumOf { bundle -> bundle.patches.size } }
|
||||
private val contentResolver: ContentResolver = app.contentResolver
|
||||
private val powerManager = app.getSystemService<PowerManager>()!!
|
||||
val sources = patchBundleRepository.sources
|
||||
val selectedSources = mutableStateListOf<PatchBundleSource>()
|
||||
|
||||
val newDownloaderPluginsAvailable =
|
||||
downloaderPluginRepository.newPluginPackageNames.map { it.isNotEmpty() }
|
||||
val newDownloaderPluginsAvailable = downloaderPluginRepository.newPluginPackageNames.map { it.isNotEmpty() }
|
||||
|
||||
/**
|
||||
* Android 11 kills the app process after granting the "install apps" permission, which is a problem for the patcher screen.
|
||||
@@ -58,16 +58,19 @@ class DashboardViewModel(
|
||||
|
||||
var updatedManagerVersion: String? by mutableStateOf(null)
|
||||
private set
|
||||
var showBatteryOptimizationsWarning by mutableStateOf(false)
|
||||
private set
|
||||
|
||||
private val bundleListEventsChannel = Channel<BundleListViewModel.Event>()
|
||||
val bundleListEventsFlow = bundleListEventsChannel.receiveAsFlow()
|
||||
val showBatteryOptimizationsWarningFlow = flow {
|
||||
while (true) {
|
||||
// There is no callback for this, so we have to poll it.
|
||||
val result = !powerManager.isIgnoringBatteryOptimizations(app.packageName)
|
||||
emit(result)
|
||||
if (!result) return@flow
|
||||
delay(500L)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
checkForManagerUpdates()
|
||||
updateBatteryOptimizationsWarning()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,6 +78,10 @@ class DashboardViewModel(
|
||||
downloaderPluginRepository.acknowledgeAllNewPlugins()
|
||||
}
|
||||
|
||||
fun dismissUpdateDialog() {
|
||||
updatedManagerVersion = null
|
||||
}
|
||||
|
||||
private suspend fun checkForManagerUpdates() {
|
||||
if (!prefs.managerAutoUpdates.get() || !networkInfo.isConnected()) return
|
||||
|
||||
@@ -83,11 +90,6 @@ class DashboardViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun updateBatteryOptimizationsWarning() {
|
||||
showBatteryOptimizationsWarning =
|
||||
!powerManager.isIgnoringBatteryOptimizations(app.packageName)
|
||||
}
|
||||
|
||||
fun setShowManagerUpdateDialogOnLaunch(value: Boolean) {
|
||||
viewModelScope.launch {
|
||||
prefs.showManagerUpdateDialogOnLaunch.update(value)
|
||||
@@ -114,20 +116,36 @@ class DashboardViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendEvent(event: BundleListViewModel.Event) {
|
||||
viewModelScope.launch { bundleListEventsChannel.send(event) }
|
||||
|
||||
fun cancelSourceSelection() {
|
||||
selectedSources.clear()
|
||||
}
|
||||
|
||||
fun cancelSourceSelection() = sendEvent(BundleListViewModel.Event.CANCEL)
|
||||
fun updateSources() = sendEvent(BundleListViewModel.Event.UPDATE_SELECTED)
|
||||
fun deleteSources() = sendEvent(BundleListViewModel.Event.DELETE_SELECTED)
|
||||
fun createLocalSource(patchBundle: Uri) =
|
||||
viewModelScope.launch {
|
||||
contentResolver.openInputStream(patchBundle)!!.use { patchesStream ->
|
||||
patchBundleRepository.createLocal(patchesStream)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("Recycle")
|
||||
fun createLocalSource(patchBundle: Uri) = viewModelScope.launch {
|
||||
patchBundleRepository.createLocal { contentResolver.openInputStream(patchBundle)!! }
|
||||
}
|
||||
fun createRemoteSource(apiUrl: String, autoUpdate: Boolean) =
|
||||
viewModelScope.launch { patchBundleRepository.createRemote(apiUrl, autoUpdate) }
|
||||
|
||||
fun createRemoteSource(apiUrl: String, autoUpdate: Boolean) = viewModelScope.launch {
|
||||
patchBundleRepository.createRemote(apiUrl, autoUpdate)
|
||||
fun delete(bundle: PatchBundleSource) =
|
||||
viewModelScope.launch { patchBundleRepository.remove(bundle) }
|
||||
|
||||
fun update(bundle: PatchBundleSource) = viewModelScope.launch {
|
||||
if (bundle !is RemotePatchBundle) return@launch
|
||||
|
||||
uiSafe(
|
||||
app,
|
||||
R.string.source_download_fail,
|
||||
RemotePatchBundle.updateFailMsg
|
||||
) {
|
||||
if (bundle.update())
|
||||
app.toast(app.getString(R.string.bundle_update_success, bundle.getName()))
|
||||
else
|
||||
app.toast(app.getString(R.string.bundle_update_unavailable, bundle.getName()))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ class DeveloperOptionsViewModel(
|
||||
private val patchBundleRepository: PatchBundleRepository
|
||||
) : ViewModel() {
|
||||
fun redownloadBundles() = viewModelScope.launch {
|
||||
uiSafe(app, R.string.patches_download_fail, RemotePatchBundle.updateFailMsg) {
|
||||
uiSafe(app, R.string.source_download_fail, RemotePatchBundle.updateFailMsg) {
|
||||
patchBundleRepository.redownloadRemoteBundles()
|
||||
}
|
||||
}
|
||||
@@ -24,4 +24,10 @@ class DeveloperOptionsViewModel(
|
||||
fun resetBundles() = viewModelScope.launch {
|
||||
patchBundleRepository.reset()
|
||||
}
|
||||
|
||||
fun disableSafeguard() = viewModelScope.launch {
|
||||
prefs.disablePatchVersionCompatCheck.update(true)
|
||||
prefs.disableSelectionWarning.update(true)
|
||||
prefs.disableUniversalPatchWarning.update(true)
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.revanced.manager.domain.manager.PreferencesManager
|
||||
import app.revanced.manager.ui.theme.Theme
|
||||
import app.revanced.manager.util.resetListItemColorsCached
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class GeneralSettingsViewModel(
|
||||
@@ -12,6 +11,5 @@ class GeneralSettingsViewModel(
|
||||
) : ViewModel() {
|
||||
fun setTheme(theme: Theme) = viewModelScope.launch {
|
||||
prefs.theme.update(theme)
|
||||
resetListItemColorsCached()
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import android.app.Application
|
||||
import android.net.Uri
|
||||
import androidx.activity.result.contract.ActivityResultContract
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -35,59 +34,6 @@ import java.nio.file.StandardCopyOption
|
||||
import kotlin.io.path.deleteExisting
|
||||
import kotlin.io.path.inputStream
|
||||
|
||||
sealed class ResetDialogState(
|
||||
@StringRes val titleResId: Int,
|
||||
@StringRes val descriptionResId: Int,
|
||||
val onConfirm: () -> Unit,
|
||||
val dialogOptionName: String? = null
|
||||
) {
|
||||
class Keystore(onConfirm: () -> Unit) : ResetDialogState(
|
||||
titleResId = R.string.regenerate_keystore,
|
||||
descriptionResId = R.string.regenerate_keystore_dialog_description,
|
||||
onConfirm = onConfirm
|
||||
)
|
||||
|
||||
class PatchSelectionAll(onConfirm: () -> Unit) : ResetDialogState(
|
||||
titleResId = R.string.patch_selection_reset_all,
|
||||
descriptionResId = R.string.patch_selection_reset_all_dialog_description,
|
||||
onConfirm = onConfirm
|
||||
)
|
||||
|
||||
class PatchSelectionPackage(dialogOptionName:String, onConfirm: () -> Unit) : ResetDialogState(
|
||||
titleResId = R.string.patch_selection_reset_package,
|
||||
descriptionResId = R.string.patch_selection_reset_package_dialog_description,
|
||||
onConfirm = onConfirm,
|
||||
dialogOptionName = dialogOptionName
|
||||
)
|
||||
|
||||
class PatchSelectionBundle(dialogOptionName: String, onConfirm: () -> Unit) : ResetDialogState(
|
||||
titleResId = R.string.patch_selection_reset_patches,
|
||||
descriptionResId = R.string.patch_selection_reset_patches_dialog_description,
|
||||
onConfirm = onConfirm,
|
||||
dialogOptionName = dialogOptionName
|
||||
)
|
||||
|
||||
class PatchOptionsAll(onConfirm: () -> Unit) : ResetDialogState(
|
||||
titleResId = R.string.patch_options_reset_all,
|
||||
descriptionResId = R.string.patch_options_reset_all_dialog_description,
|
||||
onConfirm = onConfirm
|
||||
)
|
||||
|
||||
class PatchOptionPackage(dialogOptionName:String, onConfirm: () -> Unit) : ResetDialogState(
|
||||
titleResId = R.string.patch_options_reset_package,
|
||||
descriptionResId = R.string.patch_options_reset_package_dialog_description,
|
||||
onConfirm = onConfirm,
|
||||
dialogOptionName = dialogOptionName
|
||||
)
|
||||
|
||||
class PatchOptionBundle(dialogOptionName: String, onConfirm: () -> Unit) : ResetDialogState(
|
||||
titleResId = R.string.patch_options_reset_patches,
|
||||
descriptionResId = R.string.patch_options_reset_patches_dialog_description,
|
||||
onConfirm = onConfirm,
|
||||
dialogOptionName = dialogOptionName
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
class ImportExportViewModel(
|
||||
private val app: Application,
|
||||
@@ -105,18 +51,15 @@ class ImportExportViewModel(
|
||||
private var keystoreImportPath by mutableStateOf<Path?>(null)
|
||||
val showCredentialsDialog by derivedStateOf { keystoreImportPath != null }
|
||||
|
||||
var resetDialogState by mutableStateOf<ResetDialogState?>(null)
|
||||
|
||||
val packagesWithOptions = optionsRepository.getPackagesWithSavedOptions()
|
||||
val packagesWithSelection = selectionRepository.getPackagesWithSavedSelection()
|
||||
|
||||
fun resetOptionsForPackage(packageName: String) = viewModelScope.launch {
|
||||
optionsRepository.resetOptionsForPackage(packageName)
|
||||
optionsRepository.clearOptionsForPackage(packageName)
|
||||
app.toast(app.getString(R.string.patch_options_reset_toast))
|
||||
}
|
||||
|
||||
fun resetOptionsForBundle(patchBundle: PatchBundleSource) = viewModelScope.launch {
|
||||
optionsRepository.resetOptionsForPatchBundle(patchBundle.uid)
|
||||
fun clearOptionsForBundle(patchBundle: PatchBundleSource) = viewModelScope.launch {
|
||||
optionsRepository.clearOptionsForPatchBundle(patchBundle.uid)
|
||||
app.toast(app.getString(R.string.patch_options_reset_toast))
|
||||
}
|
||||
|
||||
@@ -160,7 +103,7 @@ class ImportExportViewModel(
|
||||
private suspend fun tryKeystoreImport(cn: String, pass: String, path: Path): Boolean {
|
||||
path.inputStream().use { stream ->
|
||||
if (keystoreManager.import(cn, pass, stream)) {
|
||||
app.toast(app.getString(R.string.import_keystore_success))
|
||||
app.toast(app.getString(R.string.restore_keystore_success))
|
||||
cancelKeystoreImport()
|
||||
return true
|
||||
}
|
||||
@@ -179,7 +122,7 @@ class ImportExportViewModel(
|
||||
|
||||
fun exportKeystore(target: Uri) = viewModelScope.launch {
|
||||
keystoreManager.export(contentResolver.openOutputStream(target)!!)
|
||||
app.toast(app.getString(R.string.export_keystore_success))
|
||||
app.toast(app.getString(R.string.backup_keystore_success))
|
||||
}
|
||||
|
||||
fun regenerateKeystore() = viewModelScope.launch {
|
||||
@@ -192,16 +135,6 @@ class ImportExportViewModel(
|
||||
app.toast(app.getString(R.string.reset_patch_selection_success))
|
||||
}
|
||||
|
||||
fun resetSelectionForPackage(packageName: String) = viewModelScope.launch {
|
||||
selectionRepository.resetSelectionForPackage(packageName)
|
||||
app.toast(app.getString(R.string.reset_patch_selection_success))
|
||||
}
|
||||
|
||||
fun resetSelectionForPatchBundle(patchBundle: PatchBundleSource) = viewModelScope.launch {
|
||||
selectionRepository.resetSelectionForPatchBundle(patchBundle.uid)
|
||||
app.toast(app.getString(R.string.reset_patch_selection_success))
|
||||
}
|
||||
|
||||
fun executeSelectionAction(target: Uri) = viewModelScope.launch {
|
||||
val source = selectedBundle!!
|
||||
val action = selectionAction!!
|
||||
@@ -238,7 +171,7 @@ class ImportExportViewModel(
|
||||
override val activityArg = JSON_MIMETYPE
|
||||
override suspend fun execute(bundleUid: Int, location: Uri) = uiSafe(
|
||||
app,
|
||||
R.string.import_patch_selection_fail,
|
||||
R.string.restore_patch_selection_fail,
|
||||
"Failed to restore patch selection"
|
||||
) {
|
||||
val selection = withContext(Dispatchers.IO) {
|
||||
@@ -248,7 +181,7 @@ class ImportExportViewModel(
|
||||
}
|
||||
|
||||
selectionRepository.import(bundleUid, selection)
|
||||
app.toast(app.getString(R.string.import_patch_selection_success))
|
||||
app.toast(app.getString(R.string.restore_patch_selection_success))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,7 +190,7 @@ class ImportExportViewModel(
|
||||
override val activityArg = "selection.json"
|
||||
override suspend fun execute(bundleUid: Int, location: Uri) = uiSafe(
|
||||
app,
|
||||
R.string.export_patch_selection_fail,
|
||||
R.string.backup_patch_selection_fail,
|
||||
"Failed to backup patch selection"
|
||||
) {
|
||||
val selection = selectionRepository.export(bundleUid)
|
||||
@@ -267,7 +200,7 @@ class ImportExportViewModel(
|
||||
Json.Default.encodeToStream(selection, it)
|
||||
}
|
||||
}
|
||||
app.toast(app.getString(R.string.export_patch_selection_success))
|
||||
app.toast(app.getString(R.string.backup_patch_selection_success))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -137,8 +137,8 @@ class MainViewModel(
|
||||
updateCheck()
|
||||
}
|
||||
}
|
||||
settings.patchesChangeEnabled?.let { disableSelectionWarning ->
|
||||
prefs.disableSelectionWarning.update(disableSelectionWarning)
|
||||
settings.patchesChangeEnabled?.let { allowChangingPatchSelection ->
|
||||
prefs.allowChangingPatchSelection.update(allowChangingPatchSelection)
|
||||
}
|
||||
settings.keystore?.let { keystore ->
|
||||
val keystoreBytes = Base64.decode(keystore, Base64.DEFAULT)
|
||||
|
||||
@@ -83,9 +83,7 @@ class PatcherViewModel(
|
||||
private val savedStateHandle: SavedStateHandle = get()
|
||||
|
||||
private var installedApp: InstalledApp? = null
|
||||
private val selectedApp = input.selectedApp
|
||||
val packageName = selectedApp.packageName
|
||||
val version = selectedApp.version
|
||||
val packageName = input.selectedApp.packageName
|
||||
|
||||
var installedPackageName by savedStateHandle.saveable(
|
||||
key = "installedPackageName",
|
||||
|
||||
@@ -3,11 +3,11 @@ package app.revanced.manager.ui.viewmodel
|
||||
import android.app.Application
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.mutableStateMapOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.Saver
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateMap
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
@@ -17,9 +17,10 @@ import androidx.lifecycle.viewmodel.compose.saveable
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.domain.manager.PreferencesManager
|
||||
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||
import app.revanced.manager.patcher.patch.PatchBundleInfo
|
||||
import app.revanced.manager.patcher.patch.PatchBundleInfo.Extensions.toPatchSelection
|
||||
import app.revanced.manager.patcher.patch.PatchInfo
|
||||
import app.revanced.manager.ui.model.BundleInfo
|
||||
import app.revanced.manager.ui.model.BundleInfo.Extensions.bundleInfoFlow
|
||||
import app.revanced.manager.ui.model.BundleInfo.Extensions.toPatchSelection
|
||||
import app.revanced.manager.ui.model.navigation.SelectedApplicationInfo
|
||||
import app.revanced.manager.util.Options
|
||||
import app.revanced.manager.util.PatchSelection
|
||||
@@ -29,20 +30,15 @@ import app.revanced.manager.util.saver.persistentMapSaver
|
||||
import app.revanced.manager.util.saver.persistentSetSaver
|
||||
import app.revanced.manager.util.saver.snapshotStateMapSaver
|
||||
import app.revanced.manager.util.toast
|
||||
import kotlinx.collections.immutable.PersistentMap
|
||||
import kotlinx.collections.immutable.PersistentSet
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import kotlinx.collections.immutable.persistentSetOf
|
||||
import kotlinx.collections.immutable.toPersistentMap
|
||||
import kotlinx.collections.immutable.toPersistentSet
|
||||
import kotlinx.coroutines.CoroutineStart
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.get
|
||||
import kotlinx.collections.immutable.*
|
||||
import kotlinx.coroutines.CoroutineStart
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
@OptIn(SavedStateHandleSaveableApi::class)
|
||||
class PatchesSelectorViewModel(input: SelectedApplicationInfo.PatchesSelector.ViewModelParams) :
|
||||
@@ -54,6 +50,8 @@ class PatchesSelectorViewModel(input: SelectedApplicationInfo.PatchesSelector.Vi
|
||||
private val packageName = input.app.packageName
|
||||
val appVersion = input.app.version
|
||||
|
||||
var pendingUniversalPatchAction by mutableStateOf<(() -> Unit)?>(null)
|
||||
|
||||
var selectionWarningEnabled by mutableStateOf(true)
|
||||
private set
|
||||
var universalPatchWarningEnabled by mutableStateOf(true)
|
||||
@@ -62,24 +60,22 @@ class PatchesSelectorViewModel(input: SelectedApplicationInfo.PatchesSelector.Vi
|
||||
val allowIncompatiblePatches =
|
||||
get<PreferencesManager>().disablePatchVersionCompatCheck.getBlocking()
|
||||
val bundlesFlow =
|
||||
get<PatchBundleRepository>().scopedBundleInfoFlow(packageName, input.app.version)
|
||||
get<PatchBundleRepository>().bundleInfoFlow(packageName, input.app.version)
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
if (prefs.disableUniversalPatchCheck.get()) {
|
||||
universalPatchWarningEnabled = false
|
||||
}
|
||||
universalPatchWarningEnabled = !prefs.disableUniversalPatchWarning.get()
|
||||
|
||||
if (prefs.disableSelectionWarning.get()) {
|
||||
if (prefs.allowChangingPatchSelection.get()) {
|
||||
selectionWarningEnabled = false
|
||||
return@launch
|
||||
}
|
||||
|
||||
fun PatchBundleInfo.Scoped.hasDefaultPatches() =
|
||||
fun BundleInfo.hasDefaultPatches() =
|
||||
patchSequence(allowIncompatiblePatches).any { it.include }
|
||||
|
||||
// Don't show the warning if there are no default patches.
|
||||
selectionWarningEnabled = bundlesFlow.first().any(PatchBundleInfo.Scoped::hasDefaultPatches)
|
||||
selectionWarningEnabled = bundlesFlow.first().any(BundleInfo::hasDefaultPatches)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,7 +118,7 @@ class PatchesSelectorViewModel(input: SelectedApplicationInfo.PatchesSelector.Vi
|
||||
// This is for the required options screen.
|
||||
private val requiredOptsPatchesDeferred = viewModelScope.async(start = CoroutineStart.LAZY) {
|
||||
bundlesFlow.first().map { bundle ->
|
||||
bundle to bundle.patchSequence(allowIncompatiblePatches).filter { patch ->
|
||||
bundle to bundle.all.filter { patch ->
|
||||
val opts by lazy {
|
||||
getOptions(bundle.uid, patch).orEmpty()
|
||||
}
|
||||
@@ -135,14 +131,14 @@ class PatchesSelectorViewModel(input: SelectedApplicationInfo.PatchesSelector.Vi
|
||||
}
|
||||
val requiredOptsPatches = flow { emit(requiredOptsPatchesDeferred.await()) }
|
||||
|
||||
fun selectionIsValid(bundles: List<PatchBundleInfo.Scoped>) = bundles.any { bundle ->
|
||||
fun selectionIsValid(bundles: List<BundleInfo>) = bundles.any { bundle ->
|
||||
bundle.patchSequence(allowIncompatiblePatches).any { patch ->
|
||||
isSelected(bundle.uid, patch)
|
||||
}
|
||||
}
|
||||
|
||||
fun isSelected(bundle: Int, patch: PatchInfo) = customPatchSelection?.let { selection ->
|
||||
selection[bundle]?.contains(patch.name) == true
|
||||
selection[bundle]?.contains(patch.name) ?: false
|
||||
} ?: patch.include
|
||||
|
||||
fun togglePatch(bundle: Int, patch: PatchInfo) = viewModelScope.launch {
|
||||
@@ -159,6 +155,17 @@ class PatchesSelectorViewModel(input: SelectedApplicationInfo.PatchesSelector.Vi
|
||||
customPatchSelection = selection.put(bundle, newPatches)
|
||||
}
|
||||
|
||||
fun confirmUniversalPatchWarning() {
|
||||
universalPatchWarningEnabled = false
|
||||
|
||||
pendingUniversalPatchAction?.invoke()
|
||||
pendingUniversalPatchAction = null
|
||||
}
|
||||
|
||||
fun dismissUniversalPatchWarning() {
|
||||
pendingUniversalPatchAction = null
|
||||
}
|
||||
|
||||
fun reset() {
|
||||
patchOptions.clear()
|
||||
customPatchSelection = null
|
||||
@@ -201,8 +208,8 @@ class PatchesSelectorViewModel(input: SelectedApplicationInfo.PatchesSelector.Vi
|
||||
compatibleVersions.clear()
|
||||
}
|
||||
|
||||
fun openIncompatibleDialog(incompatiblePatch: PatchInfo) {
|
||||
compatibleVersions.addAll(incompatiblePatch.compatiblePackages?.find { it.packageName == packageName }?.versions.orEmpty())
|
||||
fun openUnsupportedDialog(unsupportedPatch: PatchInfo) {
|
||||
compatibleVersions.addAll(unsupportedPatch.compatiblePackages?.find { it.packageName == packageName }?.versions.orEmpty())
|
||||
}
|
||||
|
||||
fun toggleFlag(flag: Int) {
|
||||
@@ -210,7 +217,7 @@ class PatchesSelectorViewModel(input: SelectedApplicationInfo.PatchesSelector.Vi
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val SHOW_INCOMPATIBLE = 1 // 2^0
|
||||
const val SHOW_UNSUPPORTED = 1 // 2^0
|
||||
const val SHOW_UNIVERSAL = 2 // 2^1
|
||||
|
||||
private val optionsSaver: Saver<PersistentOptions, Options> = snapshotStateMapSaver(
|
||||
|
||||
@@ -28,14 +28,16 @@ import app.revanced.manager.domain.repository.InstalledAppRepository
|
||||
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||
import app.revanced.manager.domain.repository.PatchOptionsRepository
|
||||
import app.revanced.manager.domain.repository.PatchSelectionRepository
|
||||
import app.revanced.manager.patcher.patch.PatchBundleInfo
|
||||
import app.revanced.manager.patcher.patch.PatchBundleInfo.Extensions.toPatchSelection
|
||||
import app.revanced.manager.network.downloader.LoadedDownloaderPlugin
|
||||
import app.revanced.manager.network.downloader.ParceledDownloaderData
|
||||
import app.revanced.manager.patcher.patch.PatchBundleInfo.Extensions.requiredOptionsSet
|
||||
import app.revanced.manager.patcher.patch.PatchInfo
|
||||
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.BundleInfo
|
||||
import app.revanced.manager.ui.model.BundleInfo.Extensions.bundleInfoFlow
|
||||
import app.revanced.manager.ui.model.BundleInfo.Extensions.toPatchSelection
|
||||
import app.revanced.manager.ui.model.BundleInfo.Extensions.requiredOptionsSet
|
||||
import app.revanced.manager.ui.model.SelectedApp
|
||||
import app.revanced.manager.ui.model.navigation.Patcher
|
||||
import app.revanced.manager.ui.model.navigation.SelectedApplicationInfo
|
||||
@@ -124,19 +126,16 @@ class SelectedAppInfoViewModel(
|
||||
suggestedVersions[input.app.packageName]
|
||||
}
|
||||
|
||||
val bundleInfoFlow by derivedStateOf {
|
||||
bundleRepository.scopedBundleInfoFlow(packageName, selectedApp.version)
|
||||
}
|
||||
|
||||
var options: Options by savedStateHandle.saveable {
|
||||
val state = mutableStateOf<Options>(emptyMap())
|
||||
|
||||
viewModelScope.launch {
|
||||
if (!persistConfiguration) return@launch // TODO: save options for patched apps.
|
||||
val bundlePatches = bundleInfoFlow.first()
|
||||
.associate { it.uid to it.patches.associateBy { patch -> patch.name } }
|
||||
|
||||
options = withContext(Dispatchers.Default) {
|
||||
state.value = withContext(Dispatchers.Default) {
|
||||
val bundlePatches = bundleRepository.bundles.first()
|
||||
.mapValues { (_, bundle) -> bundle.patches.associateBy { it.name } }
|
||||
|
||||
optionsRepository.getOptions(packageName, bundlePatches)
|
||||
}
|
||||
}
|
||||
@@ -145,7 +144,7 @@ class SelectedAppInfoViewModel(
|
||||
}
|
||||
private set
|
||||
|
||||
private var selectionState: SelectionState by savedStateHandle.saveable {
|
||||
private var selectionState by savedStateHandle.saveable {
|
||||
if (input.patches != null)
|
||||
return@saveable mutableStateOf(SelectionState.Customized(input.patches))
|
||||
|
||||
@@ -153,11 +152,11 @@ class SelectedAppInfoViewModel(
|
||||
|
||||
// Try to get the previous selection if customization is enabled.
|
||||
viewModelScope.launch {
|
||||
if (!prefs.disableSelectionWarning.get()) return@launch
|
||||
if (!prefs.allowChangingPatchSelection.get()) return@launch
|
||||
|
||||
val previous = selectionRepository.getSelection(packageName)
|
||||
if (previous.values.sumOf { it.size } == 0) return@launch
|
||||
selectionState = SelectionState.Customized(previous)
|
||||
selection.value = SelectionState.Customized(previous)
|
||||
}
|
||||
|
||||
selection
|
||||
@@ -178,6 +177,10 @@ class SelectedAppInfoViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
val bundleInfoFlow by derivedStateOf {
|
||||
bundleRepository.bundleInfoFlow(packageName, selectedApp.version)
|
||||
}
|
||||
|
||||
fun showSourceSelector() {
|
||||
dismissSourceSelector()
|
||||
showSourceSelector = true
|
||||
@@ -264,48 +267,46 @@ class SelectedAppInfoViewModel(
|
||||
selectedAppInfo = info
|
||||
}
|
||||
|
||||
fun getOptionsFiltered(bundles: List<PatchBundleInfo.Scoped>) = options.filtered(bundles)
|
||||
suspend fun hasSetRequiredOptions(patchSelection: PatchSelection) = bundleInfoFlow
|
||||
.first()
|
||||
.requiredOptionsSet(
|
||||
allowIncompatible = prefs.disablePatchVersionCompatCheck.get(),
|
||||
isSelected = { bundle, patch -> patch.name in patchSelection[bundle.uid]!! },
|
||||
optionsForPatch = { bundle, patch -> options[bundle.uid]?.get(patch.name) },
|
||||
)
|
||||
|
||||
suspend fun getPatcherParams(): Patcher.ViewModelParams {
|
||||
val allowIncompatible = prefs.disablePatchVersionCompatCheck.get()
|
||||
val allowUnsupported = prefs.disablePatchVersionCompatCheck.get()
|
||||
val bundles = bundleInfoFlow.first()
|
||||
return Patcher.ViewModelParams(
|
||||
selectedApp,
|
||||
getPatches(bundles, allowIncompatible),
|
||||
getPatches(bundles, allowUnsupported),
|
||||
getOptionsFiltered(bundles)
|
||||
)
|
||||
}
|
||||
|
||||
fun getPatches(bundles: List<PatchBundleInfo.Scoped>, allowIncompatible: Boolean) =
|
||||
selectionState.patches(bundles, allowIncompatible)
|
||||
fun getOptionsFiltered(bundles: List<BundleInfo>) = options.filtered(bundles)
|
||||
|
||||
fun getPatches(bundles: List<BundleInfo>, allowUnsupported: Boolean) =
|
||||
selectionState.patches(bundles, allowUnsupported)
|
||||
|
||||
fun getCustomPatches(
|
||||
bundles: List<PatchBundleInfo.Scoped>,
|
||||
allowIncompatible: Boolean
|
||||
bundles: List<BundleInfo>,
|
||||
allowUnsupported: Boolean
|
||||
): PatchSelection? =
|
||||
(selectionState as? SelectionState.Customized)?.patches(bundles, allowIncompatible)
|
||||
(selectionState as? SelectionState.Customized)?.patches(bundles, allowUnsupported)
|
||||
|
||||
fun updateConfiguration(selection: PatchSelection?, options: Options) = viewModelScope.launch {
|
||||
val bundles = bundleInfoFlow.first()
|
||||
|
||||
fun updateConfiguration(
|
||||
selection: PatchSelection?,
|
||||
options: Options
|
||||
) = viewModelScope.launch {
|
||||
selectionState = selection?.let(SelectionState::Customized) ?: SelectionState.Default
|
||||
|
||||
val filteredOptions = options.filtered(bundleInfoFlow.first())
|
||||
val filteredOptions = options.filtered(bundles)
|
||||
this@SelectedAppInfoViewModel.options = filteredOptions
|
||||
|
||||
if (!persistConfiguration) return@launch
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
selection?.let { selectionRepository.updateSelection(packageName, it) }
|
||||
?: selectionRepository.resetSelectionForPackage(packageName)
|
||||
?: selectionRepository.clearSelection(packageName)
|
||||
|
||||
optionsRepository.saveOptions(packageName, filteredOptions)
|
||||
}
|
||||
@@ -319,37 +320,36 @@ class SelectedAppInfoViewModel(
|
||||
/**
|
||||
* Returns a copy with all nonexistent options removed.
|
||||
*/
|
||||
private fun Options.filtered(bundles: List<PatchBundleInfo.Scoped>): Options =
|
||||
buildMap options@{
|
||||
bundles.forEach bundles@{ bundle ->
|
||||
val bundleOptions = this@filtered[bundle.uid] ?: return@bundles
|
||||
private fun Options.filtered(bundles: List<BundleInfo>): Options = buildMap options@{
|
||||
bundles.forEach bundles@{ bundle ->
|
||||
val bundleOptions = this@filtered[bundle.uid] ?: return@bundles
|
||||
|
||||
val patches = bundle.patches.associateBy { it.name }
|
||||
val patches = bundle.all.associateBy { it.name }
|
||||
|
||||
this@options[bundle.uid] = buildMap bundleOptions@{
|
||||
bundleOptions.forEach patch@{ (patchName, values) ->
|
||||
// Get all valid option keys for the patch.
|
||||
val validOptionKeys =
|
||||
patches[patchName]?.options?.map { it.key }?.toSet() ?: return@patch
|
||||
this@options[bundle.uid] = buildMap bundleOptions@{
|
||||
bundleOptions.forEach patch@{ (patchName, values) ->
|
||||
// Get all valid option keys for the patch.
|
||||
val validOptionKeys =
|
||||
patches[patchName]?.options?.map { it.key }?.toSet() ?: return@patch
|
||||
|
||||
this@bundleOptions[patchName] = values.filterKeys { key ->
|
||||
key in validOptionKeys
|
||||
}
|
||||
this@bundleOptions[patchName] = values.filterKeys { key ->
|
||||
key in validOptionKeys
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sealed interface SelectionState : Parcelable {
|
||||
fun patches(bundles: List<PatchBundleInfo.Scoped>, allowIncompatible: Boolean): PatchSelection
|
||||
fun patches(bundles: List<BundleInfo>, allowUnsupported: Boolean): PatchSelection
|
||||
|
||||
@Parcelize
|
||||
data class Customized(val patchSelection: PatchSelection) : SelectionState {
|
||||
override fun patches(bundles: List<PatchBundleInfo.Scoped>, allowIncompatible: Boolean) =
|
||||
override fun patches(bundles: List<BundleInfo>, allowUnsupported: Boolean) =
|
||||
bundles.toPatchSelection(
|
||||
allowIncompatible
|
||||
allowUnsupported
|
||||
) { uid, patch ->
|
||||
patchSelection[uid]?.contains(patch.name) ?: false
|
||||
}
|
||||
@@ -357,8 +357,8 @@ private sealed interface SelectionState : Parcelable {
|
||||
|
||||
@Parcelize
|
||||
data object Default : SelectionState {
|
||||
override fun patches(bundles: List<PatchBundleInfo.Scoped>, allowIncompatible: Boolean) =
|
||||
bundles.toPatchSelection(allowIncompatible) { _, patch -> patch.include }
|
||||
override fun patches(bundles: List<BundleInfo>, allowUnsupported: Boolean) =
|
||||
bundles.toPatchSelection(allowUnsupported) { _, patch -> patch.include }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import android.content.pm.PackageInstaller
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableLongStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.core.content.ContextCompat
|
||||
@@ -43,9 +42,9 @@ class UpdateViewModel(
|
||||
private val networkInfo: NetworkInfo by inject()
|
||||
private val fs: Filesystem by inject()
|
||||
|
||||
var downloadedSize by mutableLongStateOf(0L)
|
||||
var downloadedSize by mutableStateOf(0L)
|
||||
private set
|
||||
var totalSize by mutableLongStateOf(0L)
|
||||
var totalSize by mutableStateOf(0L)
|
||||
private set
|
||||
val downloadProgress by derivedStateOf {
|
||||
if (downloadedSize == 0L || totalSize == 0L) return@derivedStateOf 0f
|
||||
@@ -90,7 +89,7 @@ class UpdateViewModel(
|
||||
totalSize = contentLength
|
||||
}
|
||||
}
|
||||
installUpdate()
|
||||
state = State.CAN_INSTALL
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -141,10 +140,10 @@ class UpdateViewModel(
|
||||
location.delete()
|
||||
}
|
||||
|
||||
enum class State(@StringRes val title: Int) {
|
||||
enum class State(@StringRes val title: Int, val showCancel: Boolean = false) {
|
||||
CAN_DOWNLOAD(R.string.update_available),
|
||||
DOWNLOADING(R.string.downloading_manager_update),
|
||||
CAN_INSTALL(R.string.ready_to_install_update),
|
||||
DOWNLOADING(R.string.downloading_manager_update, true),
|
||||
CAN_INSTALL(R.string.ready_to_install_update, true),
|
||||
INSTALLING(R.string.installing_manager_update),
|
||||
FAILED(R.string.install_update_manager_failed),
|
||||
SUCCESS(R.string.update_completed)
|
||||
|
||||
@@ -3,7 +3,6 @@ package app.revanced.manager.ui.viewmodel
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.ViewModel
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.data.platform.NetworkInfo
|
||||
import app.revanced.manager.domain.manager.PreferencesManager
|
||||
import app.revanced.manager.network.api.ReVancedAPI
|
||||
import app.revanced.manager.util.toast
|
||||
@@ -13,14 +12,10 @@ class UpdatesSettingsViewModel(
|
||||
prefs: PreferencesManager,
|
||||
private val app: Application,
|
||||
private val reVancedAPI: ReVancedAPI,
|
||||
private val network: NetworkInfo,
|
||||
) : ViewModel() {
|
||||
val managerAutoUpdates = prefs.managerAutoUpdates
|
||||
val showManagerUpdateDialogOnLaunch = prefs.showManagerUpdateDialogOnLaunch
|
||||
|
||||
val isConnected: Boolean
|
||||
get() = network.isConnected()
|
||||
|
||||
suspend fun checkForUpdates(): Boolean {
|
||||
uiSafe(app, R.string.failed_to_check_updates, "Failed to check for updates") {
|
||||
app.toast(app.getString(R.string.update_check))
|
||||
|
||||
@@ -44,10 +44,10 @@ class PM(
|
||||
) {
|
||||
private val scope = CoroutineScope(Dispatchers.IO)
|
||||
|
||||
val appList = patchBundleRepository.bundleInfoFlow.map { bundles ->
|
||||
val appList = patchBundleRepository.bundles.map { bundles ->
|
||||
val compatibleApps = scope.async {
|
||||
val compatiblePackages = bundles
|
||||
.flatMap { (_, bundle) -> bundle.patches }
|
||||
val compatiblePackages = bundles.values
|
||||
.flatMap { it.patches }
|
||||
.flatMap { it.compatiblePackages.orEmpty() }
|
||||
.groupingBy { it.packageName }
|
||||
.eachCount()
|
||||
@@ -112,7 +112,7 @@ class PM(
|
||||
app.packageManager.getPackageInfo(packageName, PackageInfoFlags.of(flags.toLong()))
|
||||
else
|
||||
app.packageManager.getPackageInfo(packageName, flags)
|
||||
} catch (_: NameNotFoundException) {
|
||||
} catch (e: NameNotFoundException) {
|
||||
null
|
||||
}
|
||||
|
||||
@@ -184,8 +184,6 @@ class PM(
|
||||
get() = PackageInstaller.SessionParams(
|
||||
PackageInstaller.SessionParams.MODE_FULL_INSTALL
|
||||
).apply {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||
setRequestUpdateOwnership(true)
|
||||
setInstallReason(PackageManager.INSTALL_REASON_USER)
|
||||
}
|
||||
|
||||
|
||||
@@ -116,10 +116,10 @@ inline fun LifecycleOwner.launchAndRepeatWithViewLifecycle(
|
||||
*/
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
inline fun <T, reified R, C> Flow<Iterable<T>>.flatMapLatestAndCombine(
|
||||
crossinline combiner: suspend (Array<R>) -> C,
|
||||
crossinline transformer: suspend (T) -> Flow<R>,
|
||||
crossinline combiner: (Array<R>) -> C,
|
||||
crossinline transformer: (T) -> Flow<R>,
|
||||
): Flow<C> = flatMapLatest { iterable ->
|
||||
combine(iterable.map { transformer(it) }) {
|
||||
combine(iterable.map(transformer)) {
|
||||
combiner(it)
|
||||
}
|
||||
}
|
||||
@@ -180,10 +180,6 @@ fun LocalDateTime.relativeTime(context: Context): String {
|
||||
|
||||
private var transparentListItemColorsCached: ListItemColors? = null
|
||||
|
||||
fun resetListItemColorsCached() {
|
||||
transparentListItemColorsCached = null
|
||||
}
|
||||
|
||||
/**
|
||||
* The default ListItem colors, but with [ListItemColors.containerColor] set to [Color.Transparent].
|
||||
*/
|
||||
|
||||
@@ -5,8 +5,11 @@
|
||||
<string name="cli">CLI</string>
|
||||
<string name="manager">Manager</string>
|
||||
|
||||
<string name="revanced_patcher">ReVanced Patcher</string>
|
||||
|
||||
<string name="plugin_host_permission_label">ReVanced Manager plugin host</string>
|
||||
<string name="plugin_host_permission_description">Used to control access to ReVanced Manager plugins. Only ReVanced Manager has this.</string>
|
||||
<string name="plugin_host_permission_description">Used to control access to ReVanced Manager
|
||||
plugins. Only ReVanced Manager has this.</string>
|
||||
|
||||
<string name="toast_copied_to_clipboard">Copied!</string>
|
||||
<string name="copy_to_clipboard">Copy to clipboard</string>
|
||||
@@ -14,141 +17,144 @@
|
||||
<string name="dashboard">Dashboard</string>
|
||||
<string name="settings">Settings</string>
|
||||
<string name="select_app">Select an app</string>
|
||||
<string name="patches_count_selected">%1$d/%2$d selected</string>
|
||||
<string name="patches_selected">%1$d/%2$d selected</string>
|
||||
|
||||
<string name="new_downloader_plugins_notification">New downloader plugins available. Click here to configure them.</string>
|
||||
<string name="unsupported_architecture_warning">Patching on this device architecture is unsupported and will most likely fail.</string>
|
||||
<string name="new_downloader_plugins_notification">New downloader plugins available. Click here
|
||||
to configure them.</string>
|
||||
<string name="unsupported_architecture_warning">Patching on this device architecture is
|
||||
unsupported and will most likely fail.</string>
|
||||
|
||||
<string name="import_">Import</string>
|
||||
<string name="import_patches">Import patches</string>
|
||||
<string name="import_bundle">Import patch bundle</string>
|
||||
<string name="bundle_patches">Bundle patches</string>
|
||||
<string name="patch_bundle_field">Patch bundle</string>
|
||||
<string name="file_field_set">Selected</string>
|
||||
<string name="file_field_not_set">Not selected</string>
|
||||
|
||||
<string name="field_not_set">Not set</string>
|
||||
|
||||
<string name="patches_missing">Missing</string>
|
||||
<string name="patches_error">Error</string>
|
||||
<string name="patches_error_description">Patches could not be loaded. Click to view the error</string>
|
||||
<string name="patches_not_downloaded">Patches has not been downloaded. Click here to download it</string>
|
||||
<string name="patches_name_default">Patches</string>
|
||||
<string name="patches_name_fallback">Unnamed</string>
|
||||
<string name="bundle_missing">Missing</string>
|
||||
<string name="bundle_error">Error</string>
|
||||
<string name="bundle_error_description">Bundle could not be loaded. Click to view the error</string>
|
||||
<string name="bundle_not_downloaded">Bundle has not been downloaded. Click here to download it</string>
|
||||
<string name="bundle_name_default">Default</string>
|
||||
<string name="bundle_name_fallback">Unnamed</string>
|
||||
|
||||
<string name="android_11_bug_dialog_title">Android 11 bug</string>
|
||||
<string name="android_11_bug_dialog_description">The app installation permission must be granted ahead of time to avoid a bug in the Android 11 system that will negatively affect the user experience.</string>
|
||||
|
||||
<string name="no_network_toast">No internet connection available</string>
|
||||
<string name="android_11_bug_dialog_description">The app installation permission must be granted
|
||||
ahead of time to avoid a bug in the Android 11 system that will negatively affect the user
|
||||
experience.</string>
|
||||
|
||||
<string name="selected_app_meta_any_version">Any available version</string>
|
||||
<string name="app_source_dialog_title">Select source</string>
|
||||
<string name="app_source_dialog_option_auto">Auto</string>
|
||||
<string name="app_source_dialog_option_auto_description">Use all available downloader to download the app</string>
|
||||
<string name="app_source_dialog_option_auto_description">Use all installed downloaders to find a
|
||||
suitable APK file</string>
|
||||
<string name="app_source_dialog_option_auto_unavailable">No plugins available</string>
|
||||
<string name="app_source_dialog_option_installed_no_root">Mounted apps cannot be patched again without root access</string>
|
||||
<string name="app_source_dialog_option_installed_version_not_suggested">Version %s does not match the suggested version</string>
|
||||
<string name="app_source_dialog_option_installed_no_root">Mounted apps cannot be patched again
|
||||
without root access</string>
|
||||
<string name="app_source_dialog_option_installed_version_not_suggested">Version %s does not
|
||||
match the suggested version</string>
|
||||
|
||||
<string name="patch_item_description">Start patching the application</string>
|
||||
<string name="patch_selector_item">Select patches</string>
|
||||
<string name="patch_selector_item">Patch selection and options</string>
|
||||
<string name="patch_selector_item_description">%d patches selected</string>
|
||||
<string name="no_patches_selected">No patches selected</string>
|
||||
|
||||
<string name="network_unavailable_warning">Your device is not connected to the internet. Downloading will fail later.</string>
|
||||
<string name="network_metered_warning">You are currently on a metered connection. Data charges from your service provider may apply.</string>
|
||||
|
||||
<string name="apk_source_selector_item">Select APK source</string>
|
||||
<string name="apk_source_auto">Using all APK downloader</string>
|
||||
<string name="apk_source_downloader">Using %s</string>
|
||||
<string name="apk_source_installed">Using installed APK</string>
|
||||
<string name="apk_source_local">Using a local APK file</string>
|
||||
<string name="apk_source_selector_item">Change source</string>
|
||||
<string name="apk_source_auto">Current: All downloaders</string>
|
||||
<string name="apk_source_downloader">Current: %s</string>
|
||||
<string name="apk_source_installed">Current: Installed</string>
|
||||
<string name="apk_source_local">Current: File</string>
|
||||
|
||||
<string name="legacy_import_failed">Could not import legacy settings</string>
|
||||
|
||||
<string name="auto_updates_dialog_title">Configure updates</string>
|
||||
<string name="auto_updates_dialog_description">Do you want ReVanced Manager to periodically check for updates for the following components?</string>
|
||||
<string name="auto_updates_dialog_description">Do you want ReVanced Manager to periodically
|
||||
check for updates for the following components?</string>
|
||||
<string name="auto_updates_dialog_manager">ReVanced Manager</string>
|
||||
<string name="auto_updates_dialog_patches">ReVanced Patches</string>
|
||||
<string name="auto_updates_dialog_note">These settings can be changed later.</string>
|
||||
|
||||
<string name="general">General</string>
|
||||
<string name="general_description">Theme, dynamic color</string>
|
||||
<string name="general_description">Appearances, Updates</string>
|
||||
|
||||
<string name="updates">Updates</string>
|
||||
<string name="updates_description">Check for updates and view changelogs</string>
|
||||
<string name="downloads">Downloads</string>
|
||||
<string name="downloads_description">Downloader plugins and downloaded apps</string>
|
||||
<string name="import_export">Import & export</string>
|
||||
<string name="import_export_description">Keystore, patch options and selection</string>
|
||||
|
||||
<string name="extensions">Extensions</string>
|
||||
<string name="extensions_description">Downloader plugins, downloaded apps</string>
|
||||
<string name="backup_restore">Backup & Restore</string>
|
||||
<string name="backup_restore_description">Keystore, Patch selections, Patch options</string>
|
||||
<string name="advanced">Advanced</string>
|
||||
<string name="advanced_description">API URL, memory limit, debugging</string>
|
||||
<string name="advanced_description">API Source, memory limits, debug logs</string>
|
||||
<string name="experimental_features">Experimental features</string>
|
||||
|
||||
|
||||
<string name="about">About</string>
|
||||
<string name="opensource_licenses">Open source licenses</string>
|
||||
<string name="opensource_licenses_description">View all the libraries used to make this application</string>
|
||||
<string name="opensource_licenses_description">View all the libraries used to make this
|
||||
application</string>
|
||||
|
||||
<string name="contributors">Contributors</string>
|
||||
<string name="contributors_description">View the contributors of ReVanced</string>
|
||||
<string name="dynamic_color">Dynamic color</string>
|
||||
<string name="dynamic_color_description">Adapt colors to the wallpaper</string>
|
||||
<string name="theme">Theme</string>
|
||||
<string name="theme_description">Choose between light or dark theme</string>
|
||||
<string name="personalized_color">Personalized color</string>
|
||||
<string name="personalized_color_description">Use color provided by your device\'s palette</string>
|
||||
<string name="theme_mode">Theme mode</string>
|
||||
<string name="theme_mode_description">Choose between light, dark, and system provided mode</string>
|
||||
<string name="safeguards">Safeguards</string>
|
||||
<string name="patch_compat_check">Disable version compatibility check</string>
|
||||
<string name="patch_compat_check_description">The check restricts patches to compatible app versions</string>
|
||||
<string name="patch_compat_check_confirmation">Selecting incompatible patches can result in a broken app.\n\nDo you want to proceed anyways?</string>
|
||||
<string name="suggested_version_safeguard">Require suggested app version</string>
|
||||
<string name="suggested_version_safeguard_description">Enforce selection of the suggested app version</string>
|
||||
<string name="suggested_version_safeguard_confirmation">Selecting an app that is not the suggested version may cause unexpected issues.\n\nDo you want to proceed anyways?</string>
|
||||
<string name="patch_selection_safeguard">Allow changing patch selection and options</string>
|
||||
<string name="patch_selection_safeguard_description">Do not prevent selecting or deselecting patches and customization of options</string>
|
||||
<string name="patch_selection_safeguard_confirmation">Changing the selection of patches may cause unexpected issues.\n\nEnable anyways?</string>
|
||||
<string name="universal_patches_safeguard">Allow using universal patches</string>
|
||||
<string name="universal_patches_safeguard_description">Do not prevent using universal patches</string>
|
||||
<string name="universal_patches_safeguard_confirmation">Universal patches are not as well tested as those that target specific apps.\n\nEnable anyways?</string>
|
||||
<string name="import_keystore">Import keystore</string>
|
||||
<string name="import_keystore_description">Import a custom keystore</string>
|
||||
<string name="import_keystore_dialog_title">Enter keystore credentials</string>
|
||||
<string name="import_keystore_dialog_description">You\'ll need enter the keystore’s credentials to import it.</string>
|
||||
<string name="import_keystore_dialog_alias_field">Username (Alias)</string>
|
||||
<string name="import_keystore_dialog_password_field">Password</string>
|
||||
<string name="import_keystore_dialog_button">Import</string>
|
||||
<string name="import_keystore_wrong_credentials">Wrong keystore credentials</string>
|
||||
<string name="import_keystore_success">Imported keystore</string>
|
||||
<string name="export_keystore">Export keystore</string>
|
||||
<string name="export_keystore_description">Export the current keystore</string>
|
||||
<string name="export_keystore_unavailable">No keystore to export</string>
|
||||
<string name="export_keystore_success">Exported keystore</string>
|
||||
<string name="regenerate_keystore">Regenerate keystore</string>
|
||||
<string name="regenerate_keystore_description">Generate a new keystore</string>
|
||||
<string name="regenerate_keystore_dialog_description">You are about to regenerate your keystore the manager will use during the patching process.\n\nYou will not be able to update the previously installed apps from this source.</string>
|
||||
<string name="allow_compatibility_mixing">Allow unsupported compatibility</string>
|
||||
<string name="allow_compatibility_mixing_description">Permit apps and patches to be mixed in
|
||||
unsupported state</string>
|
||||
<string name="allow_compatibility_mixing_confirmation">Selecting incompatible patches can result
|
||||
in a broken app.\n\nAllow anyways?</string>
|
||||
<string name="patch_selection_safeguard">Allow changing patch selection</string>
|
||||
<string name="patch_selection_safeguard_description">Permit selecting or deselecting patches
|
||||
from default</string>
|
||||
<string name="patch_selection_safeguard_confirmation">Changing the selection of patches may
|
||||
cause unexpected issues.\n\nAllow anyways?</string>
|
||||
<string name="universal_patches_safeguard">Allow universal patch</string>
|
||||
<string name="universal_patches_safeguard_description">Permit selecting app\'s generic patch</string>
|
||||
<string name="universal_patches_safeguard_confirmation">Universal patch are not as well tested
|
||||
as those that target specific apps.\n\nAllow anyways?</string>
|
||||
<string name="backup">Backup</string>
|
||||
<string name="restore">Restore</string>
|
||||
<string name="keystore">Keystore</string>
|
||||
<string name="patch_selection">Patch selection</string>
|
||||
<string name="patch_options">Patch options</string>
|
||||
<string name="restore_keystore_description">Restore keystore from external source</string>
|
||||
<string name="restore_keystore_dialog_title">Enter keystore credentials</string>
|
||||
<string name="restore_keystore_dialog_description">You\'ll need enter the keystore’s credentials
|
||||
to restore it.</string>
|
||||
<string name="restore_keystore_dialog_alias_field">Username (Alias)</string>
|
||||
<string name="restore_keystore_dialog_password_field">Password</string>
|
||||
<string name="restore_keystore_dialog_button">Import</string>
|
||||
<string name="restore_keystore_wrong_credentials">Wrong keystore credentials</string>
|
||||
<string name="restore_keystore_success">Keystore successfully restored</string>
|
||||
<string name="backup_keystore_description">Export app’s keystore into usable file</string>
|
||||
<string name="backup_keystore_unavailable">No keystore to backup</string>
|
||||
<string name="backup_keystore_success">Keystore successfully backed up</string>
|
||||
<string name="regenerate_keystore">Regeneration</string>
|
||||
<string name="regenerate_keystore_description">Replace current keystore with new one</string>
|
||||
<string name="regenerate_keystore_success">The keystore has been successfully replaced</string>
|
||||
<string name="import_patch_selection">Import patch selection</string>
|
||||
<string name="import_patch_selection_description">Import patch selection from a JSON file</string>
|
||||
<string name="import_patch_selection_fail">Could not import patch selection: %s</string>
|
||||
<string name="import_patch_selection_success">Imported patch selection</string>
|
||||
<string name="export_patch_selection">Export patch selection</string>
|
||||
<string name="export_patch_selection_description">Export patch selection to a JSON file</string>
|
||||
<string name="export_patch_selection_fail">Could not export patch selection: %s</string>
|
||||
<string name="export_patch_selection_success">Exported patch selection</string>
|
||||
<string name="restore_patch_selection_description">Export app’s patch selections into usable
|
||||
file</string>
|
||||
<string name="restore_patch_selection_fail">Could not import patch selection: %s</string>
|
||||
<string name="restore_patch_selection_success">Patch selection successfully restored</string>
|
||||
<string name="backup_patch_selection_description">Import app’s patch selections from external
|
||||
source</string>
|
||||
<string name="backup_patch_selection_fail">Could not backup patch selection: %s</string>
|
||||
<string name="backup_patch_selection_success">Patch selection successfully restored</string>
|
||||
<string name="reset_patch_selection">Reset patch selection</string>
|
||||
<string name="reset_patch_selection_description">Reset the stored patch selection</string>
|
||||
<string name="reset_patch_options">Reset patch options</string>
|
||||
<string name="reset_patch_options_description">Reset the stored patch options</string>
|
||||
<string name="reset_patch_selection_success">Patch selection has been reset</string>
|
||||
<string name="patch_selection_reset_all">Reset patch selection globally</string>
|
||||
<string name="patch_selection_reset_all_dialog_description">You are about to reset all the patch selections. You will need to manually select each patch again.</string>
|
||||
<string name="patch_selection_reset_all_description">Resets all the patch selections</string>
|
||||
<string name="patch_selection_reset_package">Reset patch selection for app</string>
|
||||
<string name="patch_selection_reset_package_dialog_description">You are about to reset the patch selection for the app \"%s\". You will have to manually select each patch again.</string>
|
||||
<string name="patch_selection_reset_package_description">Resets patch selection for a single app</string>
|
||||
<string name="patch_selection_reset_patches">Reset patch selection (single)</string>
|
||||
<string name="patch_selection_reset_patches_dialog_description">You are about to reset the patch selection for \"%s\". You will have to manually select each patch again.</string>
|
||||
<string name="patch_selection_reset_patches_description">Resets the patch selection for a specific collection of patches</string>
|
||||
<string name="patch_options_reset_package">Reset patch options for app</string>
|
||||
<string name="patch_options_reset_package_dialog_description">You are about to reset the patch options for the app \"%s\". You will have to reapply each option again.</string>
|
||||
<string name="patch_options_reset_package_description">Resets patch options for a single app</string>
|
||||
<string name="patch_options_reset_patches">Reset patch options (single)</string>
|
||||
<string name="patch_options_reset_patches_dialog_description">You are about to reset the patch options for \"%s\". You will have to reapply each option again.</string>
|
||||
<string name="patch_options_reset_patches_description">Resets the patch options for a specific collection of patches</string>
|
||||
<string name="patch_options_reset_all">Reset patch options globally</string>
|
||||
<string name="patch_options_reset_all_dialog_description">You are about to reset patch options. You will have to reapply each option again.</string>
|
||||
<string name="patch_options_reset_all_description">Resets all patch options</string>
|
||||
<string name="reset_patch_selection_success">Patch selection successfully reset</string>
|
||||
<string name="patch_options_reset_package">Reset for app</string>
|
||||
<string name="patch_options_reset_package_description">Use the default patch options
|
||||
configuration for a single app</string>
|
||||
<string name="patch_options_reset_bundle">Reset for bundle</string>
|
||||
<string name="patch_options_reset_bundle_description">Use the default patch options
|
||||
configuration for all patches in a bundle</string>
|
||||
<string name="patch_options_reset_all_description">Use the default patch options configuration</string>
|
||||
<string name="downloader_plugins">Plugins</string>
|
||||
<string name="downloader_plugin_state_trusted">Trusted</string>
|
||||
<string name="downloader_plugin_state_failed">Failed to load. Click for more details</string>
|
||||
@@ -156,13 +162,11 @@
|
||||
<string name="downloader_plugin_trust_dialog_title">Trust plugin?</string>
|
||||
<string name="downloader_plugin_revoke_trust_dialog_title">Revoke trust?</string>
|
||||
<string name="downloader_plugin_trust_dialog_body">Package name: %1$s\nSignature (SHA-256): %2$s</string>
|
||||
<string name="downloader_plugin_delete_apps_title">Delete selected apps</string>
|
||||
<string name="downloader_plugin_delete_apps_description">Are you sure you want to delete the selected apps?</string>
|
||||
<string name="downloader_settings_no_apps">No downloaded apps found</string>
|
||||
|
||||
<string name="search_apps">Search apps…</string>
|
||||
<string name="loading_body">Loading…</string>
|
||||
<string name="downloading_patches">Downloading patches…</string>
|
||||
<string name="downloading_patches">Downloading patch bundle…</string>
|
||||
|
||||
<string name="options">Options</string>
|
||||
<string name="ok">OK</string>
|
||||
@@ -190,63 +194,79 @@
|
||||
<string name="dark">Dark</string>
|
||||
<string name="appearance">Appearance</string>
|
||||
<string name="downloaded_apps">Downloaded apps</string>
|
||||
<string name="process_runtime">Run Patcher in another process (experimental)</string>
|
||||
<string name="process_runtime_description">This is faster and allows Patcher to use more memory.</string>
|
||||
<string name="process_runtime">Run patcher in another process</string>
|
||||
<string name="process_runtime_description">Faster and allows patcher to use more memory</string>
|
||||
<string name="process_runtime_memory_limit">Patcher process memory limit</string>
|
||||
<string name="process_runtime_memory_limit_description">The max amount of memory that the Patcher process can use (in megabytes)</string>
|
||||
<string name="process_runtime_memory_limit_description">The max amount of memory that the
|
||||
patcher process can use (in megabytes)</string>
|
||||
<string name="debug_logs_export">Export debug logs</string>
|
||||
<string name="debug_logs_export_read_failed">Failed to read logs (exit code %d)</string>
|
||||
<string name="debug_logs_export_failed">Failed to export logs</string>
|
||||
<string name="debug_logs_export_success">Exported logs</string>
|
||||
<string name="api_url">API URL</string>
|
||||
<string name="api_url_description">The API used to download necessary files.</string>
|
||||
<string name="api_url_dialog_title">Change API URL</string>
|
||||
<string name="api_url_dialog_description">Change the API URL of ReVanced Manager. ReVanced Manager uses the API to download patches and updates.</string>
|
||||
<string name="api_url_dialog_warning">ReVanced Manager connects to the API to download patches and updates. Make sure that you trust it.</string>
|
||||
<string name="api_url">API Source</string>
|
||||
<string name="api_url_dialog_title">Set custom API Source</string>
|
||||
<string name="api_url_dialog_description">Set the API Source of ReVanced Manager. ReVanced
|
||||
Manager uses the API to download patches and updates.</string>
|
||||
<string name="api_url_dialog_warning">ReVanced Manager connects to the API to download patches
|
||||
and updates. Make sure that you trust it.</string>
|
||||
<string name="api_url_dialog_save">Set</string>
|
||||
<string name="api_url_dialog_reset">Reset API URL</string>
|
||||
<string name="api_url_dialog_reset">Reset API Source</string>
|
||||
<string name="testing">Testing</string>
|
||||
<string name="disable_safeguard">Disable all safeguards</string>
|
||||
<string name="device">Device</string>
|
||||
<string name="device_android_version">Android version</string>
|
||||
<string name="device_model">Model</string>
|
||||
<string name="device_architectures">CPU Architectures</string>
|
||||
<string name="device_memory_limit">Memory limits</string>
|
||||
<string name="device_memory_limit_format">%1$dMB (Normal) - %2$dMB (Large)</string>
|
||||
<string name="patches_force_download">Force download all patches</string>
|
||||
<string name="patches_reset">Reset patches</string>
|
||||
<string name="patch_bundles_section">Patch bundles</string>
|
||||
<string name="patch_bundles_force_download">Force download all patch bundles</string>
|
||||
<string name="patch_bundles_reset">Reset patch bundles</string>
|
||||
<string name="patching">Patching</string>
|
||||
<string name="signing">Signing</string>
|
||||
<string name="storage">Storage</string>
|
||||
<string name="no_patch_found">No patch can be found. Check your patches</string>
|
||||
<string name="patches_unavailable">No patches are available. Check your bundles</string>
|
||||
<string name="tab_apps">Apps</string>
|
||||
<string name="tab_patches">Patches</string>
|
||||
<string name="tab_bundles">Patch bundles</string>
|
||||
<string name="delete">Delete</string>
|
||||
<string name="refresh">Refresh</string>
|
||||
<string name="continue_anyways">Continue anyways</string>
|
||||
<string name="download_another_version">Download another version</string>
|
||||
<string name="download_app">Download app</string>
|
||||
<string name="download_apk">Download APK file</string>
|
||||
<string name="patches_download_fail">Failed to download patches: %s</string>
|
||||
<string name="patches_replace_fail">Failed to import patches: %s</string>
|
||||
<string name="source_download_fail">Failed to download patch bundle: %s</string>
|
||||
<string name="source_replace_fail">Failed to load updated patch bundle: %s</string>
|
||||
<string name="no_patched_apps_found">No patched apps found</string>
|
||||
<string name="tap_on_patches">Tap on the patches to get more information about them</string>
|
||||
<string name="patches_selected">%s selected</string>
|
||||
<string name="incompatible_patches">Incompatible patches</string>
|
||||
<string name="bundles_selected">%s selected</string>
|
||||
<string name="unsupported_patches">Incompatible patches</string>
|
||||
<string name="universal_patches">Universal patches</string>
|
||||
<string name="patch_selection_reset_toast">Patch selection and options has been reset to recommended defaults</string>
|
||||
<string name="patch_selection_reset_toast">Patch selection and options has been reset to
|
||||
recommended defaults</string>
|
||||
<string name="patch_options_reset_toast">Patch options have been reset</string>
|
||||
<string name="non_suggested_version_warning_title">Non suggested version</string>
|
||||
<string name="non_suggested_version_warning_description">The version of the app you have selected does not match the suggested version.\nPlease use the suggested version: %s\n\nTo continue anyway, disable \"Require suggested app version\" in the advanced settings.</string>
|
||||
<string name="non_suggested_version_warning_description">The version of the app you have
|
||||
selected does not match the suggested version.\nPlease use the suggested version: %s\n\nTo
|
||||
continue anyway, disable \"Require suggested app version\" in the advanced settings.</string>
|
||||
<string name="selection_warning_title">Stop using defaults?</string>
|
||||
<string name="selection_warning_description">It is recommended to use the default patch selection and options. Changing them may result in unexpected issues.\n\nYou need to turn on \"Allow changing patch selection\" in the advanced settings before toggling patches.</string>
|
||||
<string name="universal_patch_warning_description">Universal patches have a more generalized use and do not work as reliably as patches that target specific apps. You may encounter issues while using them.\n\nYou need to turn on \"Allow using universal patches\" in the advanced settings before using universal patches.</string>
|
||||
<string name="this_version">This version</string>
|
||||
<string name="selection_warning_description">It is recommended to use the default patch
|
||||
selection and options. Changing them may result in unexpected issues.\n\nYou need to turn on
|
||||
\"Allow changing patch selection\" in the advanced settings before toggling patches.</string>
|
||||
<string name="universal_patch_warning_description">Universal patches have a more generalized use
|
||||
and do not work as reliably as patches that target specific apps. You may encounter issues
|
||||
while using them.\n\nThis warning can be disabled in the advanced settings.</string>
|
||||
<string name="supported">This version</string>
|
||||
<string name="universal">Any app</string>
|
||||
<string name="unsupported">Unsupported</string>
|
||||
<string name="search_patches">Search patches</string>
|
||||
<string name="app_version_not_compatible">This patch is not compatible with the selected app version (%1$s).\n\nIt is only compatible with the following version(s): %2$s.</string>
|
||||
<string name="app_not_supported">This patch is not compatible with the selected app version
|
||||
(%1$s).\n\nIt only supports the following version(s): %2$s.</string>
|
||||
<string name="continue_with_version">Continue with this version?</string>
|
||||
<string name="version_not_compatible">Not all patches are compatible with this version (%s). Do you want to continue anyway?</string>
|
||||
<string name="version_not_supported">Not all patches support this version (%s). Do you want to
|
||||
continue anyway?</string>
|
||||
<string name="download_application">Download application?</string>
|
||||
<string name="app_not_installed">The app you selected isn\'t installed. Do you want to download it?</string>
|
||||
<string name="app_not_installed">The app you selected isn\'t installed. Do you want to download
|
||||
it?</string>
|
||||
<string name="failed_to_load_apk">Failed to load APK</string>
|
||||
<string name="loading">Loading…</string>
|
||||
<string name="not_installed">Not installed</string>
|
||||
@@ -275,8 +295,9 @@
|
||||
<string name="downloader_invalid_version">Downloader did not fetch the correct version</string>
|
||||
<string name="downloader_app_not_found">Downloader did not find the app</string>
|
||||
<string name="downloader_error">Downloader error: %s</string>
|
||||
<string name="downloader_no_plugins_installed">No downloader installed.</string>
|
||||
<string name="downloader_no_plugins_available">There are downloader installed but none is trusted. Check your settings.</string>
|
||||
<string name="downloader_no_plugins_installed">No plugins installed.</string>
|
||||
<string name="downloader_no_plugins_available">No trusted plugins available for use. Check your
|
||||
settings.</string>
|
||||
<string name="already_patched">Already patched</string>
|
||||
|
||||
<string name="patch_selector_sheet_filter_title">Filter</string>
|
||||
@@ -304,7 +325,8 @@
|
||||
<string name="save_apk_success">APK Saved</string>
|
||||
<string name="sign_fail">Failed to sign APK: %s</string>
|
||||
<string name="save_logs">Save logs</string>
|
||||
<string name="plugin_activity_dialog_body">User interaction is required in order to proceed with this plugin.</string>
|
||||
<string name="plugin_activity_dialog_body">User interaction is required in order to proceed with
|
||||
this plugin.</string>
|
||||
<string name="select_install_type">Select installation type</string>
|
||||
|
||||
<string name="patcher_step_group_preparing">Preparing</string>
|
||||
@@ -314,10 +336,7 @@
|
||||
<string name="patcher_step_group_saving">Saving</string>
|
||||
<string name="patcher_step_write_patched">Write patched APK file</string>
|
||||
<string name="patcher_step_sign_apk">Sign patched APK file</string>
|
||||
<string name="patcher_notification_title">Patching in progress…</string>
|
||||
<string name="patcher_notification_text">Tap to return to the patcher</string>
|
||||
<string name="patcher_stop_confirm_title">Stop patcher</string>
|
||||
<string name="patcher_stop_confirm_description">Are you sure you want to stop the patching process?</string>
|
||||
<string name="patcher_notification_message">Patching in progress…</string>
|
||||
<string name="execute_patches">Execute patches</string>
|
||||
<string name="executing_patch">Execute %s</string>
|
||||
<string name="failed_to_execute_patch">Failed to execute %s</string>
|
||||
@@ -345,34 +364,37 @@
|
||||
<string name="submit_feedback_description">Help us improve this application</string>
|
||||
<string name="developer_options">Developer options</string>
|
||||
<string name="developer_options_description">Options for debugging issues</string>
|
||||
<string name="patches_update_success">Update successful</string>
|
||||
<string name="patches_update_unavailable">No update available</string>
|
||||
<string name="view_patches">View patches</string>
|
||||
<string name="patches_view_any_version">Any version</string>
|
||||
<string name="patches_view_any_package">Any package</string>
|
||||
<string name="patches_delete_single_dialog_description">Are you sure you want to delete \"%s\"?</string>
|
||||
<string name="patches_delete_multiple_dialog_description">Are you sure you want to delete the selected patches?</string>
|
||||
<string name="bundle_input_source_url">Source URL</string>
|
||||
<string name="bundle_update_success">Successfully updated %s</string>
|
||||
<string name="bundle_update_unavailable">No update available for %s</string>
|
||||
<string name="bundle_auto_update">Auto update</string>
|
||||
<string name="bundle_auto_update_description">Automatically update this bundle when ReVanced
|
||||
starts</string>
|
||||
<string name="bundle_view_patches">View patches</string>
|
||||
<string name="bundle_view_patches_any_version">Any version</string>
|
||||
<string name="bundle_view_patches_any_package">Any package</string>
|
||||
|
||||
<string name="about_revanced_manager">About ReVanced Manager</string>
|
||||
<string name="revanced_manager_description">ReVanced Manager is an Android application that uses ReVanced Patcher to patch Android apps. It allows you to download and patch apps with custom patches, and manage the patching process.</string>
|
||||
<string name="developer_options_taps">%d taps remaining</string>
|
||||
<string name="developer_options_enabled">Developer options enabled</string>
|
||||
<string name="developer_options_already_enabled">Developer options are already enabled</string>
|
||||
<string name="revanced_manager_description">ReVanced Manager is an application designed to work
|
||||
with ReVanced Patcher, which allows for long-lasting patches to be created for Android apps.
|
||||
The patching system is designed to automatically work with new versions of apps with minimal
|
||||
maintenance.</string>
|
||||
<string name="update_available">An update is available</string>
|
||||
<string name="current_version">Current version: %s</string>
|
||||
<string name="new_version">New version: %s</string>
|
||||
<string name="ready_to_install_update">Ready to install update</string>
|
||||
<string name="update_completed">Update installed</string>
|
||||
<string name="install_update_manager_failed">Failed to install update</string>
|
||||
<string name="manual_update_check">Check for updates</string>
|
||||
<string name="manual_update_check_description">Manually check for updates</string>
|
||||
<string name="update_checking_manager">Check for updates on launch</string>
|
||||
<string name="update_checking_manager_description">Check for new versions of ReVanced Manager when the application starts</string>
|
||||
<string name="check_for_update">Check for update</string>
|
||||
<string name="check_for_update_auto_description">Automatically check for new version when the
|
||||
app launched</string>
|
||||
<string name="changelog">View changelogs</string>
|
||||
<string name="changelog_loading">Loading changelog</string>
|
||||
<string name="changelog_download_fail">Failed to download changelog: %s</string>
|
||||
<string name="changelog_description">Check out the latest changes in this update</string>
|
||||
<string name="battery_optimization_notification">Battery optimizations must be turned off in order for ReVanced Manager to work correctly in the background. Click here to turn off optimizations.</string>
|
||||
<string name="battery_optimization_notification">Battery optimizations must be turned off in
|
||||
order for ReVanced Manager to work correctly in the background. Click here to turn off
|
||||
optimizations.</string>
|
||||
<string name="installing_manager_update">Installing update…</string>
|
||||
<string name="downloading_manager_update">Downloading update…</string>
|
||||
<string name="download_manager_failed">Failed to download update: %s</string>
|
||||
@@ -381,7 +403,8 @@
|
||||
<string name="save_with_count">Save (%1$s)</string>
|
||||
<string name="update">Update</string>
|
||||
<string name="empty">Empty</string>
|
||||
<string name="installing_message">Tap on <b>Update</b> when prompted.\nReVanced Manager will close when updating.</string>
|
||||
<string name="installing_message">Tap on <b>Update</b> when prompted. \n ReVanced Manager will
|
||||
close when updating.</string>
|
||||
<string name="no_changelogs_found">No changelogs found</string>
|
||||
<string name="just_now">Just now</string>
|
||||
<string name="minutes_ago">%sm ago</string>
|
||||
@@ -397,17 +420,22 @@
|
||||
<string name="no_update_available">No update available</string>
|
||||
<string name="update_check">Checking for updates…</string>
|
||||
<string name="dismiss_temporary">Not now</string>
|
||||
<string name="update_available_dialog_description">A new version of ReVanced Manager (%s) is available.</string>
|
||||
<string name="update_available_dialog_description">A new version of ReVanced Manager (%s) is
|
||||
available.</string>
|
||||
<string name="failed_to_download_update">Failed to download update: %s</string>
|
||||
<string name="download">Download</string>
|
||||
<string name="download_confirmation_metered">You are currently on a metered connection, and data charges from your service provider may apply.\n\nDo you still want to continue?</string>
|
||||
<string name="download_confirmation_metered">You are currently on a metered connection, and data
|
||||
charges from your service provider may apply.\n\nDo you still want to continue?</string>
|
||||
<string name="download_update_confirmation">Download update?</string>
|
||||
<string name="no_contributors_found">No contributors found</string>
|
||||
<string name="select">Select</string>
|
||||
<string name="select_deselect_all">Select or deselect all</string>
|
||||
<string name="select_patches_type_dialog_description">Add new patches from URL or local files</string>
|
||||
<string name="local_patches_description">Add patches from local storage.</string>
|
||||
<string name="remote_patches_description">Add patches from URL. Patches can automatically update.</string>
|
||||
<string name="select_bundle_type_dialog_title">Add new bundle</string>
|
||||
<string name="select_bundle_type_dialog_description">Add a new bundle from a URL or storage</string>
|
||||
<string name="local_bundle_description">Import local files from your storage, does not
|
||||
automatically update</string>
|
||||
<string name="remote_bundle_description">Import remote files from a URL, can automatically
|
||||
update</string>
|
||||
<string name="recommended">Recommended</string>
|
||||
|
||||
<string name="installation_failed_dialog_title">Installation failed</string>
|
||||
@@ -418,13 +446,20 @@
|
||||
<string name="installation_invalid_dialog_title">Installation invalid</string>
|
||||
<string name="installation_storage_issue_dialog_title">Not enough storage</string>
|
||||
<string name="installation_timeout_dialog_title">Installation timed out</string>
|
||||
<string name="installation_failed_description">The installation failed due to an unknown reason. Try again?</string>
|
||||
<string name="installation_aborted_description">The installation was cancelled manually. Try again?</string>
|
||||
<string name="installation_blocked_description">The installation was blocked. Review your device security settings and try again.</string>
|
||||
<string name="installation_conflict_description">The installation was prevented by an existing installation of the app. Uninstall the installed app and try again?</string>
|
||||
<string name="installation_incompatible_description">The app is incompatible with this device. Use an APK that is compatible by this device and try again.</string>
|
||||
<string name="installation_invalid_description">The app is invalid. Uninstall the app and try again?</string>
|
||||
<string name="installation_storage_issue_description">The app could not be installed due to insufficient storage. Free up some space and try again.</string>
|
||||
<string name="installation_failed_description">The installation failed due to an unknown reason.
|
||||
Try again?</string>
|
||||
<string name="installation_aborted_description">The installation was cancelled manually. Try
|
||||
again?</string>
|
||||
<string name="installation_blocked_description">The installation was blocked. Review your device
|
||||
security settings and try again.</string>
|
||||
<string name="installation_conflict_description">The installation was prevented by an existing
|
||||
installation of the app. Uninstall the installed app and try again?</string>
|
||||
<string name="installation_incompatible_description">The app is incompatible with this device.
|
||||
Use an APK that is supported by this device and try again.</string>
|
||||
<string name="installation_invalid_description">The app is invalid. Uninstall the app and try
|
||||
again?</string>
|
||||
<string name="installation_storage_issue_description">The app could not be installed due to
|
||||
insufficient storage. Free up some space and try again.</string>
|
||||
<string name="installation_timeout_description">The installation took too long. Try again?</string>
|
||||
<string name="reinstall">Reinstall</string>
|
||||
<string name="show">Show</string>
|
||||
@@ -432,17 +467,17 @@
|
||||
<string name="about_device">About device</string>
|
||||
<string name="enter_url">Enter URL</string>
|
||||
<string name="next">Next</string>
|
||||
<string name="add_patch_bundle">Add patch bundle</string>
|
||||
<string name="bundle_url">Bundle URL</string>
|
||||
<string name="auto_update">Auto update</string>
|
||||
<string name="add_patches">Add patches</string>
|
||||
<string name="auto_update_description">Automatically update when a new version is available</string>
|
||||
<string name="patches_url">Patches URL</string>
|
||||
<string name="incompatible_patches_dialog">These patches are not compatible with the selected app version (%1$s).\n\nClick on the patches to see more details.</string>
|
||||
<string name="incompatible_patch">Incompatible patch</string>
|
||||
<string name="unsupported_patches_dialog">These patches are not compatible with the selected app
|
||||
version (%1$s).\n\nClick on the patches to see more details.</string>
|
||||
<string name="unsupported_patch">Unsupported patch</string>
|
||||
<string name="any_version">Any</string>
|
||||
<string name="never_show_again">Never show again</string>
|
||||
<string name="show_manager_update_dialog_on_launch">Show update message on launch</string>
|
||||
<string name="show_manager_update_dialog_on_launch_description">Shows a popup notification whenever there is a new update available on launch.</string>
|
||||
<string name="show_manager_update_dialog_on_launch_description">Shows a popup notification
|
||||
whenever there is a new update available on launch.</string>
|
||||
<string name="failed_to_import_keystore">Failed to import keystore</string>
|
||||
<string name="export">Export</string>
|
||||
<string name="confirm">Confirm</string>
|
||||
</resources>
|
||||
</resources>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user