Compare commits

..

105 Commits

Author SHA1 Message Date
a5d09b2bf5 Consolidate optional root device requirements in docs 2025-02-19 23:27:05 +07:00
8731e7267a Fix grammar in GitHub Registry section 2025-02-19 23:24:01 +07:00
c2a942b46c Update docs/2_4_settings.md
Co-authored-by: Ax333l <main@axelen.xyz>
2025-01-29 17:50:04 +07:00
b9401b5f89 Update docs/3_troubleshooting.md
Co-authored-by: Ax333l <main@axelen.xyz>
2025-01-29 17:49:08 +07:00
0eee869b99 Update docs/2_4_settings.md
Co-authored-by: Ax333l <main@axelen.xyz>
2025-01-29 17:48:43 +07:00
15cfeb6ba7 Update docs/0_prerequisites.md 2025-01-29 17:47:48 +07:00
d47819cae7 Update docs/developer/3_verifying.md
Co-authored-by: Ax333l <main@axelen.xyz>
2025-01-29 00:09:26 +07:00
351e00c380 format 2025-01-27 17:31:42 +07:00
046179db1b Remove copy-pasta 2025-01-27 17:30:13 +07:00
5993dc34cb Add manage app 2025-01-27 17:29:56 +07:00
91f6672711 Add linkj 2025-01-27 16:47:56 +07:00
d3748cf305 Easily distinguishness 2025-01-27 16:47:36 +07:00
db304a70c8 Merge suggestions from Copilot 2025-01-27 16:41:18 +07:00
8f9ea54790 Update ref 2025-01-27 15:43:16 +07:00
a53a0bd2d4 Comfortable docs edit viewing 2025-01-27 15:36:30 +07:00
1c6a9dbbbd Developer - Transparency 2025-01-27 15:34:59 +07:00
802330c062 Verification step 2025-01-27 15:34:47 +07:00
d1ef14a25a format 2025-01-27 15:34:37 +07:00
617481f26f End of section 2025-01-27 15:34:25 +07:00
6598db5078 Update Manager guide!! 2025-01-27 15:33:36 +07:00
5b790a86e2 Comfortable docs viewing 2025-01-19 19:53:32 +07:00
bec0d823dc grammartical corerct 2025-01-19 19:53:20 +07:00
2616576b3d grammartical corerct 2025-01-19 19:53:12 +07:00
44fc4551aa Add settings location 2025-01-19 19:49:00 +07:00
6dfb2a2de0 Try or report 2025-01-19 19:42:02 +07:00
03c91687ae Split to section 2025-01-19 19:41:36 +07:00
91e06cbce6 OOM error troubleshooting 2025-01-19 19:41:21 +07:00
b0624a3544 OOM error troubleshooting 2025-01-19 19:40:06 +07:00
04a2a78914 Change wording to remove redundancy 2025-01-19 19:24:38 +07:00
4364f2e4c2 Standardised download site 2025-01-19 19:24:13 +07:00
177b716fd0 🔮 Merge repository updated to latest snapshot!
Script Execution UTC Time: null

Signed-off-by: validcube <pun.butrach@gmail.com>
2025-01-19 18:45:06 +07:00
818dc09aa4 build: Bump AGP to 8.8.0
build: Bump AGP to 8.8.0
2025-01-19 17:47:56 +07:00
a762969966 docs: Merge documentation from Flutter to Compose 2025-01-19 17:08:07 +07:00
74338931b8 feat: Redesign the patches screen (#2381) 2025-01-18 17:03:38 +01:00
0ab424bfdb fix: available updates dialog list item color 2025-01-05 00:12:00 +01:00
fff1a41fee refactor: use EventEffect for legacy import 2025-01-05 00:08:29 +01:00
7644a74648 feat: add required options screen (#2378) 2025-01-03 22:26:40 +01:00
9db3bd5b3f feat: Add confirm dialogs when toggling dangerous settings (#2072)
Co-authored-by: Ax333l <main@axelen.xyz>
2024-12-23 16:35:27 +01:00
b81bd17fbc chore: add .kotlin to gitignore 2024-12-23 14:40:28 +01:00
cf3866f892 fix: remove battery optimization notification if user grants the permission 2024-12-23 14:39:57 +01:00
5d3a81f4b9 feat: switch to androidx.navigation (#2362) 2024-12-23 14:31:31 +01:00
f9831d4da5 refactor: remove unnecessary function 2024-12-23 13:13:08 +01:00
8a20d8cf9b fix: contributors screen repository name 2024-12-22 22:32:15 +01:00
49f75f9edd fix: process death resilience and account for android 11 bug (#2355) 2024-12-22 22:28:54 +01:00
d987845bba docs: Actually link to documentation 2024-11-24 22:11:46 +07:00
50c46dc20d docs(readme): Merge building to contributing section 2024-11-24 22:09:45 +07:00
5125084279 docs: Merge valid suggestions from Copilot review
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2024-11-24 19:38:53 +07:00
911201ad9f Signed off to switch branch
Signed-off-by: validcube <pun.butrach@gmail.com>
2024-10-18 01:53:51 +07:00
8442bf2e14 🔮 Merge repository updated to latest snapshot!
Script Execution UTC Time: null

Signed-off-by: validcube <pun.butrach@gmail.com>
2024-10-01 17:08:55 +07:00
05bf940cdf nitpicking on style 2024-08-18 20:43:29 +07:00
3bbad156c4 part: 10 (1/?) 2024-08-18 20:41:45 +07:00
db4ce6bb77 docs: Use correct asset directory 2024-08-18 20:01:45 +07:00
a290369d8d docs: Pull change from ReVanced Manager Flutter 2024-08-18 20:00:44 +07:00
adf5f9f6e8 docs: Pull change from ReVanced Branding 2024-08-18 20:00:17 +07:00
ace6701aaf 🔮 Merge repository updated to latest snapshot!
Script Execution UTC Time: null

Signed-off-by: validcube <pun.butrach@gmail.com>
2024-08-18 18:56:57 +07:00
94de170490 docs: part 9.5
merge changes from `dev` (flutter)
2023-12-09 21:40:43 +07:00
3c56db4121 Merge branch 'compose-dev' into docs/readme 2023-12-09 21:35:37 +07:00
62bb0d34ce docs(accessibility): part 9.4
Heading 1 -> 2 -> 3 not 1 -> 3
2023-11-09 00:01:02 +07:00
1521d21e4e docs: part 9.3 2023-11-08 23:46:31 +07:00
35996b6c69 docs: part 9.2
Co-authored-by: Palm <palmpasuthorn@gmail.com>
2023-11-08 22:41:12 +07:00
d32a213457 docs: part 9.1 2023-11-08 22:31:19 +07:00
2a3395ce15 docs: part 9
This commit removes the developer version, preventing this PR from being locked up with additional developer experiences.
2023-11-08 22:25:33 +07:00
944b57c336 docs: part 8.7
Co-authored-by: KobeW50 <84587632+KobeW50@users.noreply.github.com>
2023-11-08 22:19:54 +07:00
bc09af9d0b docs: part 8.6.1 (test) 2023-10-23 15:28:56 +07:00
0419b2f86b docs: part 8.6 2023-10-23 15:16:34 +07:00
2f31fc7d6e docs: part 8.5
TODO: see PR #1411 suggestions

Co-authored-by: Palm <palmpasuthorn@gmail.com>
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
2023-10-23 15:07:17 +07:00
dcf51c1777 docs: part 8.4-2 2023-10-23 00:52:19 +07:00
94eb893a01 docs: 8.4-1.1 2023-10-23 00:51:58 +07:00
af49457bfc docs: part 8.4-1 2023-10-23 00:50:38 +07:00
a8682d671d docs: part 8.3 2023-10-23 00:49:46 +07:00
10815c8f73 docs: part 7.2 2023-10-23 00:49:28 +07:00
891fb57531 docs: part 8.1 2023-10-22 12:41:45 +07:00
07ee005613 docs: part 8
Co-authored-by: Ushie <ushiekane@gmail.com>
2023-10-22 12:38:49 +07:00
a3c48d14ab docs: part 7.1 2023-10-21 22:45:41 +07:00
f14b697769 docs: part 7
Co-authored-by: Ushie <ushiekane@gmail.com>
2023-10-21 22:43:40 +07:00
d06fb08239 docs: part 6.9 2023-10-21 20:46:06 +07:00
cf9a14c762 docs: part 6.8 2023-10-21 20:35:25 +07:00
7b49af21d0 docs: part 6.7 2023-10-21 20:32:34 +07:00
18774084c9 docs: part 6.6 2023-10-21 20:30:53 +07:00
a70ad3d666 docs: part 6.5 2023-10-21 20:13:42 +07:00
0d2d879603 docs 6.4 2023-10-21 20:13:12 +07:00
8acdc17cc4 docs: part 6.3 2023-10-21 19:57:45 +07:00
1f5331dbfe docs: part 6.2 2023-10-21 19:57:00 +07:00
98747f4afb docs: part 6.1 2023-10-21 19:54:44 +07:00
2f782b4963 docs: part 6 2023-10-21 19:45:49 +07:00
3c083edfda docs: part 5.2 2023-10-21 19:45:00 +07:00
7bf1a5af65 docs: part 5.1 2023-10-21 19:44:05 +07:00
8a3d163266 docs: part 5 2023-10-21 19:39:20 +07:00
d58fd96bb0 docs: part 4.10 2023-10-21 18:29:51 +07:00
259f76379e docs: part 4.9 2023-10-21 17:50:59 +07:00
b8378fbb02 docs: part 4.8 2023-10-21 17:44:01 +07:00
2f0cdfff59 docs: part 4.7 2023-10-21 17:35:29 +07:00
cd5787a4f7 docs: part 4.6 2023-10-21 17:30:32 +07:00
a280fc2446 docs: part 4.5.1 (test) 2023-10-21 17:21:21 +07:00
a503f4830b docs: part 4.5 2023-10-21 17:15:39 +07:00
f732df1b9a docs: part 4.4 2023-10-21 17:11:43 +07:00
9b9525ece5 docs: part 4.3 2023-10-21 17:06:43 +07:00
e0dfbaf4a3 docs: part 4.2 2023-10-21 17:05:53 +07:00
949b1dad0e docs: part 4.1 2023-10-21 17:03:28 +07:00
830a666a58 docs: part 4 2023-10-21 16:58:45 +07:00
26b9652135 docs: part 3 2023-10-21 16:38:29 +07:00
85c6e7283d docs: add ReVanced assets 2023-10-21 16:21:19 +07:00
67603f64cd docs: part 2.1 2023-10-21 16:09:55 +07:00
676eb1e9e2 docs: part 2 2023-10-09 17:09:34 +07:00
e7b94868d6 docs: merge branding to readme (SEE TODO)
TODO:
2023-10-08 16:47:28 +07:00
60 changed files with 1824 additions and 893 deletions

1
.gitignore vendored
View File

@ -9,3 +9,4 @@
.cxx
local.properties
.kotlin/

111
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,111 @@
<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>&nbsp;&nbsp;&nbsp;
<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>&nbsp;&nbsp;&nbsp;
<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>&nbsp;&nbsp;&nbsp;
<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>&nbsp;&nbsp;&nbsp;
<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>&nbsp;&nbsp;&nbsp;
<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>&nbsp;&nbsp;&nbsp;
<a href="https://www.youtube.com/@ReVanced">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
</picture>
</a>
<br>
<br>
Continuing the legacy of Vanced
</p>
# 👋 Contribution guidelines
Welcome to contribution guidelines, this document contains
everything you'll need to contribute to ReVanced Manager (and might even possibly apply to our other project like ReVanced Patches!)
There are many ways to contribute like writing docs and code, opening issues,
helping people out in our community, translating or sponsoring our project, etc.
## 📖 Resources to help you get started
* The [documentation](/docs/developer/README.md) provides steps to build ReVanced Manager from source
* Our [backlog](https://github.com/orgs/ReVanced/projects/12) is where we keep track of what we're working on
* [Issues](https://github.com/ReVanced/revanced-manager/issues) are where we keep track of bugs and feature requests
## 🙏 Submitting a feature request
Features can be requested by opening an issue using the
[feature request issue template](https://github.com/ReVanced/revanced-manager/issues/new?assignees=&labels=feature-request&projects=&template=feature-issue.yml&title=feat%3A+%3Ctitle%3E).
> [!NOTE]
> 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 the ReVanced Manager app, open an issue using the
[bug report issue template](https://github.com/ReVanced/revanced-manager/issues/new?assignees=&labels=bug&projects=&template=bug-issue.yml&title=bug%3A+%3Ctitle%3E).
## 🌐 Submitting translations
You can contribute translations at translate.revanced.app
> [!TIP]
> * Try to keep the translated text roughly the same length as the original.
> * Consider opting for gender-neutral words for language with variations of words based on gender.
## 🧑‍💻 Developing for ReVanced Manager
See the guidelines for developing for ReVanced Manager [here](/docs/developer/README.md)
## 🔒 Submitting a vulnerability
See the guideline for reporting security vulnerability [here](/SECURITY.md)
## 🤚 I don't want to do any of that but I want to contribute either way
You can still contribute by spreading positive word about us or if you'd like to sponsor us, checkout https://revanced.app/donate
to learn more about how you can sponsor via GitHub, Open Collective, and cryptocurrencies.
❤️ Thank you for considering contributing to ReVanced Manager,
ReVanced

119
README.md
View File

@ -1,55 +1,104 @@
# ReVanced Manager (Compose Rewrite)
<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>&nbsp;&nbsp;&nbsp;
<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>&nbsp;&nbsp;&nbsp;
<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>&nbsp;&nbsp;&nbsp;
<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>&nbsp;&nbsp;&nbsp;
<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>&nbsp;&nbsp;&nbsp;
<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>&nbsp;&nbsp;&nbsp;
<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>
[![GitHub license](https://img.shields.io/github/license/revanced/revanced-manager)](../../blob/main/LICENSE)
[![GitHub last commit](https://img.shields.io/github/last-commit/revanced/revanced-manager/compose-dev)](https://github.com/ReVanced/revanced-manager/commits/compose-dev)
[![GitHub commit activity](https://img.shields.io/github/commit-activity/w/revanced/revanced-manager/compose-dev)](https://github.com/ReVanced/revanced-manager/commits/compose-dev)
# 💊 ReVanced Manager
_(Yet another)_ rewrite of the ReVanced Manager using Kotlin and Jetpack Compose.
[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/ReVanced/revanced-manager/release.yml)](https://github.com/ReVanced/revanced-manager/actions/workflows/release.yml)
[![GPLv3 License](https://img.shields.io/badge/License-GPL%20v3-yellow.svg)](#-license)
## Design system
Application to use ReVanced on Android
In this rewrite, we are adopting the latest Material Design principles and guidelines by using Material 3 and Material You.
## ❓ About
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.
ReVanced Manager is an application that uses [ReVanced Patcher](https://github.com/revanced/revanced-patcher) to patch Android apps.
### Why Material 3?
## 💪 Features
* **Consistent design language**
* **Improved accessibility**
* **Better user experience**
Some of the features ReVanced Manager provides are:
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.
- 💉 **Patch apps**: Apply any patch of your choice to Android apps
- 📱 **Portable**: ReVanced Patcher that fits in your pocket
- 🤗 **Simple UI**: Quickly understand the ins and outs of ReVanced Manager
- 🛠️ **Customization**: Configurable API, custom sources, language, signing keystore, theme and more
## 🔽 Download
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)
You can get the most recent version of ReVanced Manager by downloading from
the [ReVanced site](https://revanced.app/download).
## 📝 Prerequisites
Learn how to use ReVanced Manager by following the [documentation](/docs).
For a list of prerequisites, refer to [docs/0_prerequisites.md](docs/0_prerequisites.md)
## 📚 Everything else
## 🔴 Issues
### 📙 Contributing
For suggestions and bug reports, open an issue [here](https://github.com/revanced/revanced-manager/issues/new/choose).
Thank you for considering contributing to ReVanced Manager.
## 🌐 Translation
The [contribution guidelines](CONTRIBUTING.md) provides information you'll need to open an issue, develop for ReVanced Manager and translations.
[![Crowdin](https://badges.crowdin.net/revanced/localized.svg)](https://crowdin.com/project/revanced)
### 📄 Documentation
We're accepting translations on [Crowdin](https://translate.revanced.app)
You can find the documentation for ReVanced Manager [here](/docs).
## 🛠 Building Manager from source
## License
For instructions on how to build ReVanced Manager from source, refer to [docs/4_building.md](docs/4_building.md)
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.

View File

@ -13,8 +13,8 @@
<br>
<a href="https://revanced.app/">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="assets/revanced-logo/revanced-logo-round.svg" />
<img height="24px" src="assets/revanced-logo/revanced-logo-round.svg" />
<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>&nbsp;&nbsp;&nbsp;
<a href="https://github.com/ReVanced">
@ -64,15 +64,20 @@ This document describes how to report security vulnerabilities for ReVanced Mana
## 🚨 Reporting a Vulnerability
Please open an issue in our [advisory tracker](https://github.com/ReVanced/revanced-manager/security/advisories/new) or reach out privately to us on [Discord](https://discord.gg/revanced).
Please open an issue in our [advisory tracker](https://github.com/ReVanced/revanced-manager/security/advisories/new)
or reach out privately to us on [Discord](https://discord.gg/revanced).
If a vulnerability is confirmed and accepted, you can join our [Discord](https://discord.gg/revanced) server to receive a special contributor role.
If a vulnerability is confirmed and accepted, they will be published and
you can join our [Discord](https://discord.gg/revanced) server to receive a
special contributor role.
### ⏳ Supported Versions
| Version | Branch | Supported |
| ------- | ------------|------------------- |
| v1.18.0 | main | :white_check_mark: |
| latest | dev | :white_check_mark: |
| latest | compose-dev | :white_check_mark: |
| Version | Branch | Supported |
|------------------------------------------|---------------|--------------------|
| ![Latest stable release][LatestRelBadge] | `main` | :white_check_mark: |
| ![Latest version][LatestVerBadge] | `dev` | :white_check_mark: |
| ![Latest version][LatestVerBadge] | `compose-dev` | :white_check_mark: |
[LatestRelBadge]: https://img.shields.io/github/v/release/ReVanced/revanced-manager?style=for-the-badge "Latest stable release"
[LatestVerBadge]: https://img.shields.io/badge/version-latest-brightgreen?style=for-the-badge "Latest version"

View File

@ -126,6 +126,7 @@ dependencies {
implementation(libs.compose.livedata)
implementation(libs.compose.material.icons.extended)
implementation(libs.compose.material3)
implementation(libs.navigation.compose)
// Accompanist
implementation(libs.accompanist.drawablepainter)
@ -173,11 +174,9 @@ dependencies {
// Koin
implementation(libs.koin.android)
implementation(libs.koin.compose)
implementation(libs.koin.compose.navigation)
implementation(libs.koin.workmanager)
// Compose Navigation
implementation(libs.reimagined.navigation)
// Licenses
implementation(libs.about.libraries)

View File

@ -1,36 +1,43 @@
package app.revanced.manager
import android.content.ActivityNotFoundException
import android.os.Bundle
import android.os.Parcelable
import androidx.activity.ComponentActivity
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.WindowCompat
import app.revanced.manager.ui.destination.Destination
import app.revanced.manager.ui.destination.SettingsDestination
import app.revanced.manager.ui.screen.AppSelectorScreen
import app.revanced.manager.ui.screen.DashboardScreen
import app.revanced.manager.ui.screen.InstalledAppInfoScreen
import app.revanced.manager.ui.screen.PatcherScreen
import app.revanced.manager.ui.screen.SelectedAppInfoScreen
import app.revanced.manager.ui.screen.SettingsScreen
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.navigation
import androidx.navigation.compose.rememberNavController
import androidx.navigation.toRoute
import app.revanced.manager.ui.model.navigation.*
import app.revanced.manager.ui.screen.*
import app.revanced.manager.ui.screen.settings.*
import app.revanced.manager.ui.screen.settings.update.ChangelogsScreen
import app.revanced.manager.ui.screen.settings.update.UpdatesSettingsScreen
import app.revanced.manager.ui.theme.ReVancedManagerTheme
import app.revanced.manager.ui.theme.Theme
import app.revanced.manager.ui.viewmodel.MainViewModel
import app.revanced.manager.ui.viewmodel.SelectedAppInfoViewModel
import app.revanced.manager.util.EventEffect
import dev.olshevski.navigation.reimagined.AnimatedNavHost
import dev.olshevski.navigation.reimagined.NavBackHandler
import dev.olshevski.navigation.reimagined.navigate
import dev.olshevski.navigation.reimagined.pop
import dev.olshevski.navigation.reimagined.popUpTo
import dev.olshevski.navigation.reimagined.rememberNavController
import kotlinx.coroutines.launch
import org.koin.androidx.compose.koinViewModel
import org.koin.androidx.compose.navigation.koinNavViewModel
import org.koin.core.parameter.parametersOf
import org.koin.androidx.compose.koinViewModel as getComposeViewModel
import org.koin.androidx.viewmodel.ext.android.getViewModel as getAndroidViewModel
import org.koin.androidx.viewmodel.ext.android.getViewModel as getActivityViewModel
class MainActivity : ComponentActivity() {
@ExperimentalAnimationApi
@ -41,90 +48,260 @@ class MainActivity : ComponentActivity() {
enableEdgeToEdge()
installSplashScreen()
val vm: MainViewModel = getAndroidViewModel()
vm.importLegacySettings(this)
val vm: MainViewModel = getActivityViewModel()
setContent {
val launcher = rememberLauncherForActivityResult(
ActivityResultContracts.StartActivityForResult(),
onResult = vm::applyLegacySettings
)
val theme by vm.prefs.theme.getAsState()
val dynamicColor by vm.prefs.dynamicColor.getAsState()
EventEffect(vm.legacyImportActivityFlow) {
try {
launcher.launch(it)
} catch (_: ActivityNotFoundException) {
}
}
ReVancedManagerTheme(
darkTheme = theme == Theme.SYSTEM && isSystemInDarkTheme() || theme == Theme.DARK,
dynamicColor = dynamicColor
) {
val navController =
rememberNavController<Destination>(startDestination = Destination.Dashboard)
NavBackHandler(navController)
EventEffect(vm.appSelectFlow) { app ->
navController.navigate(Destination.SelectedApplicationInfo(app))
}
AnimatedNavHost(
controller = navController
) { destination ->
when (destination) {
is Destination.Dashboard -> DashboardScreen(
onSettingsClick = { navController.navigate(Destination.Settings()) },
onAppSelectorClick = { navController.navigate(Destination.AppSelector) },
onUpdateClick = {
navController.navigate(Destination.Settings(SettingsDestination.Update()))
},
onDownloaderPluginClick = {
navController.navigate(Destination.Settings(SettingsDestination.Downloads))
},
onAppClick = { installedApp ->
navController.navigate(
Destination.InstalledApplicationInfo(
installedApp
)
)
}
)
is Destination.InstalledApplicationInfo -> InstalledAppInfoScreen(
onPatchClick = vm::selectApp,
onBackClick = { navController.pop() },
viewModel = getComposeViewModel { parametersOf(destination.installedApp) }
)
is Destination.Settings -> SettingsScreen(
onBackClick = { navController.pop() },
startDestination = destination.startDestination
)
is Destination.AppSelector -> AppSelectorScreen(
onSelect = vm::selectApp,
onStorageSelect = vm::selectApp,
onBackClick = { navController.pop() }
)
is Destination.SelectedApplicationInfo -> SelectedAppInfoScreen(
onPatchClick = { app, patches, options ->
navController.navigate(
Destination.Patcher(
app, patches, options
)
)
},
onBackClick = navController::pop,
vm = getComposeViewModel {
parametersOf(
SelectedAppInfoViewModel.Params(
destination.selectedApp,
destination.patchSelection
)
)
}
)
is Destination.Patcher -> PatcherScreen(
onBackClick = { navController.popUpTo { it is Destination.Dashboard } },
vm = getComposeViewModel { parametersOf(destination) }
)
}
}
ReVancedManager(vm)
}
}
}
}
@Composable
private fun ReVancedManager(vm: MainViewModel) {
val navController = rememberNavController()
EventEffect(vm.appSelectFlow) { app ->
navController.navigateComplex(
SelectedApplicationInfo,
SelectedApplicationInfo.ViewModelParams(app)
)
}
NavHost(
navController = navController,
startDestination = Dashboard,
) {
composable<Dashboard> {
DashboardScreen(
onSettingsClick = { navController.navigate(Settings) },
onAppSelectorClick = {
navController.navigate(AppSelector)
},
onUpdateClick = {
navController.navigate(Update())
},
onDownloaderPluginClick = {
navController.navigate(Settings.Downloads)
},
onAppClick = { packageName ->
navController.navigate(InstalledApplicationInfo(packageName))
}
)
}
composable<InstalledApplicationInfo> {
val data = it.toRoute<InstalledApplicationInfo>()
InstalledAppInfoScreen(
onPatchClick = vm::selectApp,
onBackClick = navController::popBackStack,
viewModel = koinViewModel { parametersOf(data.packageName) }
)
}
composable<AppSelector> {
AppSelectorScreen(
onSelect = vm::selectApp,
onStorageSelect = vm::selectApp,
onBackClick = navController::popBackStack
)
}
composable<Patcher> {
PatcherScreen(
onBackClick = {
navController.navigate(route = Dashboard) {
launchSingleTop = true
popUpTo<Dashboard> {
inclusive = false
}
}
},
vm = koinViewModel { parametersOf(it.getComplexArg<Patcher.ViewModelParams>()) }
)
}
composable<Update> {
val data = it.toRoute<Update>()
UpdateScreen(
onBackClick = navController::popBackStack,
vm = koinViewModel { parametersOf(data.downloadOnScreenEntry) }
)
}
navigation<SelectedApplicationInfo>(startDestination = SelectedApplicationInfo.Main) {
composable<SelectedApplicationInfo.Main> {
val parentBackStackEntry = navController.navGraphEntry(it)
val data =
parentBackStackEntry.getComplexArg<SelectedApplicationInfo.ViewModelParams>()
val viewModel =
koinNavViewModel<SelectedAppInfoViewModel>(viewModelStoreOwner = parentBackStackEntry) {
parametersOf(data)
}
SelectedAppInfoScreen(
onBackClick = navController::popBackStack,
onPatchClick = {
it.lifecycleScope.launch {
navController.navigateComplex(
Patcher,
viewModel.getPatcherParams()
)
}
},
onPatchSelectorClick = { app, patches, options ->
navController.navigateComplex(
SelectedApplicationInfo.PatchesSelector,
SelectedApplicationInfo.PatchesSelector.ViewModelParams(
app,
patches,
options
)
)
},
onRequiredOptions = { app, patches, options ->
navController.navigateComplex(
SelectedApplicationInfo.RequiredOptions,
SelectedApplicationInfo.PatchesSelector.ViewModelParams(
app,
patches,
options
)
)
},
vm = viewModel
)
}
composable<SelectedApplicationInfo.PatchesSelector> {
val data =
it.getComplexArg<SelectedApplicationInfo.PatchesSelector.ViewModelParams>()
val selectedAppInfoVm = koinNavViewModel<SelectedAppInfoViewModel>(
viewModelStoreOwner = navController.navGraphEntry(it)
)
PatchesSelectorScreen(
onBackClick = navController::popBackStack,
onSave = { patches, options ->
selectedAppInfoVm.updateConfiguration(patches, options)
navController.popBackStack()
},
vm = koinViewModel { parametersOf(data) }
)
}
composable<SelectedApplicationInfo.RequiredOptions> {
val data =
it.getComplexArg<SelectedApplicationInfo.PatchesSelector.ViewModelParams>()
val selectedAppInfoVm = koinNavViewModel<SelectedAppInfoViewModel>(
viewModelStoreOwner = navController.navGraphEntry(it)
)
RequiredOptionsScreen(
onBackClick = navController::popBackStack,
onContinue = { patches, options ->
selectedAppInfoVm.updateConfiguration(patches, options)
it.lifecycleScope.launch {
navController.navigateComplex(
Patcher,
selectedAppInfoVm.getPatcherParams()
)
}
},
vm = koinViewModel { parametersOf(data) }
)
}
}
navigation<Settings>(startDestination = Settings.Main) {
composable<Settings.Main> {
SettingsScreen(
onBackClick = navController::popBackStack,
navigate = navController::navigate
)
}
composable<Settings.General> {
GeneralSettingsScreen(onBackClick = navController::popBackStack)
}
composable<Settings.Advanced> {
AdvancedSettingsScreen(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)
}
composable<Settings.About> {
AboutSettingsScreen(
onBackClick = navController::popBackStack,
navigate = navController::navigate
)
}
composable<Settings.Changelogs> {
ChangelogsScreen(onBackClick = navController::popBackStack)
}
composable<Settings.Contributors> {
ContributorScreen(onBackClick = navController::popBackStack)
}
composable<Settings.Licenses> {
LicensesScreen(onBackClick = navController::popBackStack)
}
composable<Settings.DeveloperOptions> {
DeveloperOptionsScreen(onBackClick = navController::popBackStack)
}
}
}
}
@Composable
private fun NavController.navGraphEntry(entry: NavBackStackEntry) =
remember(entry) { getBackStackEntry(entry.destination.parent!!.id) }
// Androidx Navigation does not support storing complex types in route objects, so we have to store them inside the saved state handle of the back stack entry instead.
private fun <T : Parcelable, R : ComplexParameter<T>> NavController.navigateComplex(
route: R,
data: T
) {
navigate(route)
getBackStackEntry(route).savedStateHandle["args"] = data
}
private fun <T : Parcelable> NavBackStackEntry.getComplexArg() = savedStateHandle.get<T>("args")!!

View File

@ -1,18 +1,15 @@
package app.revanced.manager.data.room.apps.installed
import android.os.Parcelable
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import app.revanced.manager.R
import kotlinx.parcelize.Parcelize
enum class InstallType(val stringResource: Int) {
DEFAULT(R.string.default_install),
MOUNT(R.string.mount_install)
}
@Parcelize
@Entity(tableName = "installed_app")
data class InstalledApp(
@PrimaryKey
@ -20,4 +17,4 @@ data class InstalledApp(
@ColumnInfo(name = "original_package_name") val originalPackageName: String,
@ColumnInfo(name = "version") val version: String,
@ColumnInfo(name = "install_type") val installType: InstallType
) : Parcelable
)

View File

@ -9,7 +9,7 @@ val viewModelModule = module {
viewModelOf(::DashboardViewModel)
viewModelOf(::SelectedAppInfoViewModel)
viewModelOf(::PatchesSelectorViewModel)
viewModelOf(::SettingsViewModel)
viewModelOf(::GeneralSettingsViewModel)
viewModelOf(::AdvancedSettingsViewModel)
viewModelOf(::AppSelectorViewModel)
viewModelOf(::PatcherViewModel)

View File

@ -12,11 +12,12 @@ import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import app.revanced.manager.R
import app.revanced.manager.ui.component.haptics.HapticCheckbox
import app.revanced.manager.util.transparentListItemColors
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AvailableUpdateDialog(
onDismiss: () -> Unit,
@ -70,10 +71,11 @@ fun AvailableUpdateDialog(
Text(stringResource(R.string.never_show_again))
},
leadingContent = {
CompositionLocalProvider(LocalMinimumInteractiveComponentEnforcement provides false) {
CompositionLocalProvider(LocalMinimumInteractiveComponentSize provides Dp.Unspecified) {
HapticCheckbox(checked = dontShowAgain, onCheckedChange = { dontShowAgain = it })
}
}
},
colors = transparentListItemColors
)
}
},

View File

@ -0,0 +1,61 @@
package app.revanced.manager.ui.component
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandIn
import androidx.compose.animation.shrinkOut
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Done
import androidx.compose.material3.FilterChip
import androidx.compose.material3.FilterChipDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.SelectableChipColors
import androidx.compose.material3.SelectableChipElevation
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Shape
@Composable
fun CheckedFilterChip(
selected: Boolean,
onClick: () -> Unit,
label: @Composable () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
trailingIcon: @Composable (() -> Unit)? = null,
shape: Shape = FilterChipDefaults.shape,
colors: SelectableChipColors = FilterChipDefaults.filterChipColors(),
elevation: SelectableChipElevation? = FilterChipDefaults.filterChipElevation(),
border: BorderStroke? = FilterChipDefaults.filterChipBorder(enabled, selected),
interactionSource: MutableInteractionSource? = null
) {
FilterChip(
selected = selected,
onClick = onClick,
label = label,
modifier = modifier,
enabled = enabled,
leadingIcon = {
AnimatedVisibility(
visible = selected,
enter = expandIn(expandFrom = Alignment.CenterStart),
exit = shrinkOut(shrinkTowards = Alignment.CenterStart)
) {
Icon(
modifier = Modifier.size(FilterChipDefaults.IconSize),
imageVector = Icons.Filled.Done,
contentDescription = null,
)
}
},
trailingIcon = trailingIcon,
shape = shape,
colors = colors,
elevation = elevation,
border = border,
interactionSource = interactionSource
)
}

View File

@ -0,0 +1,60 @@
package app.revanced.manager.ui.component
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SearchBar
import androidx.compose.material3.SearchBarColors
import androidx.compose.material3.SearchBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.unit.dp
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SearchBar(
query: String,
onQueryChange: (String) -> Unit,
expanded: Boolean,
onExpandedChange: (Boolean) -> Unit,
placeholder: (@Composable () -> Unit)? = null,
leadingIcon: @Composable (() -> Unit)? = null,
trailingIcon: @Composable (() -> Unit)? = null,
content: @Composable ColumnScope.() -> Unit
) {
val colors = SearchBarColors(
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
dividerColor = MaterialTheme.colorScheme.outline
)
val keyboardController = LocalSoftwareKeyboardController.current
Box(modifier = Modifier.fillMaxWidth()) {
SearchBar(
modifier = Modifier.align(Alignment.Center),
inputField = {
SearchBarDefaults.InputField(
modifier = Modifier.sizeIn(minWidth = 380.dp),
query = query,
onQueryChange = onQueryChange,
onSearch = {
keyboardController?.hide()
},
expanded = expanded,
onExpandedChange = onExpandedChange,
placeholder = placeholder,
leadingIcon = leadingIcon,
trailingIcon = trailingIcon
)
},
expanded = expanded,
onExpandedChange = onExpandedChange,
colors = colors,
content = content
)
}
}

View File

@ -16,6 +16,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import app.revanced.manager.R
import app.revanced.manager.ui.component.AlertDialogExtended
@ -218,7 +219,7 @@ fun ImportBundleStep(
),
headlineContent = { Text(stringResource(R.string.auto_update)) },
leadingContent = {
CompositionLocalProvider(LocalMinimumInteractiveComponentEnforcement provides false) {
CompositionLocalProvider(LocalMinimumInteractiveComponentSize provides Dp.Unspecified) {
HapticCheckbox(
checked = autoUpdate,
onCheckedChange = {

View File

@ -8,6 +8,7 @@ import androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
@ -141,13 +142,19 @@ private inline fun <T : Any> WithOptionEditor(
}
@Composable
fun <T : Any> OptionItem(option: Option<T>, value: T?, setValue: (T?) -> Unit) {
fun <T : Any> OptionItem(
option: Option<T>,
value: T?,
setValue: (T?) -> Unit,
) {
val editor = remember(option.type, option.presets) {
@Suppress("UNCHECKED_CAST")
val baseOptionEditor =
optionEditors.getOrDefault(option.type, UnknownTypeEditor) as OptionEditor<T>
if (option.type != typeOf<Boolean>() && option.presets != null) PresetOptionEditor(baseOptionEditor)
if (option.type != typeOf<Boolean>() && option.presets != null) PresetOptionEditor(
baseOptionEditor
)
else baseOptionEditor
}
@ -155,7 +162,15 @@ fun <T : Any> OptionItem(option: Option<T>, value: T?, setValue: (T?) -> Unit) {
ListItem(
modifier = Modifier.clickable(onClick = ::clickAction),
headlineContent = { Text(option.title) },
supportingContent = { Text(option.description) },
supportingContent = {
Column {
Text(option.description)
if (option.required && value == null) Text(
stringResource(R.string.option_required),
color = MaterialTheme.colorScheme.error
)
}
},
trailingContent = { ListItemTrailingContent() }
)
}

View File

@ -0,0 +1,54 @@
package app.revanced.manager.ui.component.settings
import androidx.annotation.StringRes
import androidx.compose.runtime.Composable
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.Modifier
import androidx.compose.ui.res.stringResource
import app.revanced.manager.domain.manager.base.Preference
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@Composable
fun SafeguardBooleanItem(
modifier: Modifier = Modifier,
preference: Preference<Boolean>,
coroutineScope: CoroutineScope = rememberCoroutineScope(),
@StringRes headline: Int,
@StringRes description: Int,
@StringRes confirmationText: Int
) {
val value by preference.getAsState()
var showSafeguardWarning by rememberSaveable {
mutableStateOf(false)
}
if (showSafeguardWarning) {
SafeguardConfirmationDialog(
onDismiss = { showSafeguardWarning = false },
onConfirm = {
coroutineScope.launch { preference.update(!value) }
showSafeguardWarning = false
},
body = stringResource(confirmationText)
)
}
BooleanItem(
modifier = modifier,
value = value,
onValueChange = {
if (it != preference.default) {
showSafeguardWarning = true
} else {
coroutineScope.launch { preference.update(it) }
}
},
headline = headline,
description = description
)
}

View File

@ -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)
}
)
}

View File

@ -1,31 +0,0 @@
package app.revanced.manager.ui.destination
import android.os.Parcelable
import app.revanced.manager.data.room.apps.installed.InstalledApp
import app.revanced.manager.ui.model.SelectedApp
import app.revanced.manager.util.Options
import app.revanced.manager.util.PatchSelection
import kotlinx.parcelize.Parcelize
import kotlinx.parcelize.RawValue
sealed interface Destination : Parcelable {
@Parcelize
data object Dashboard : Destination
@Parcelize
data class InstalledApplicationInfo(val installedApp: InstalledApp) : Destination
@Parcelize
data object AppSelector : Destination
@Parcelize
data class Settings(val startDestination: SettingsDestination = SettingsDestination.Settings) : Destination
@Parcelize
data class SelectedApplicationInfo(val selectedApp: SelectedApp, val patchSelection: PatchSelection? = null) : Destination
@Parcelize
data class Patcher(val selectedApp: SelectedApp, val selectedPatches: PatchSelection, val options: @RawValue Options) : Destination
}

View File

@ -1,16 +0,0 @@
package app.revanced.manager.ui.destination
import android.os.Parcelable
import app.revanced.manager.ui.model.SelectedApp
import app.revanced.manager.util.Options
import app.revanced.manager.util.PatchSelection
import kotlinx.parcelize.Parcelize
import kotlinx.parcelize.RawValue
sealed interface SelectedAppInfoDestination : Parcelable {
@Parcelize
data object Main : SelectedAppInfoDestination
@Parcelize
data class PatchesSelector(val app: SelectedApp, val currentSelection: PatchSelection?, val options: @RawValue Options) : SelectedAppInfoDestination
}

View File

@ -1,43 +0,0 @@
package app.revanced.manager.ui.destination
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
sealed interface SettingsDestination : Parcelable {
@Parcelize
data object Settings : SettingsDestination
@Parcelize
data object General : SettingsDestination
@Parcelize
data object Advanced : SettingsDestination
@Parcelize
data object Updates : SettingsDestination
@Parcelize
data object Downloads : SettingsDestination
@Parcelize
data object ImportExport : SettingsDestination
@Parcelize
data object About : SettingsDestination
@Parcelize
data class Update(val downloadOnScreenEntry: Boolean = false) : SettingsDestination
@Parcelize
data object Changelogs : SettingsDestination
@Parcelize
data object Contributors: SettingsDestination
@Parcelize
data object Licenses: SettingsDestination
@Parcelize
data object DeveloperOptions: SettingsDestination
}

View File

@ -34,20 +34,23 @@ data class BundleInfo(
}
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
)
}
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
}
bundle.uid to patches
}
fun PatchBundleRepository.bundleInfoFlow(packageName: String, version: String?) =
sources.flatMapLatestAndCombine(
@ -78,6 +81,28 @@ data class BundleInfo(
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
}
}
}
}
}

View File

@ -0,0 +1,96 @@
package app.revanced.manager.ui.model.navigation
import android.os.Parcelable
import app.revanced.manager.ui.model.SelectedApp
import app.revanced.manager.util.Options
import app.revanced.manager.util.PatchSelection
import kotlinx.parcelize.Parcelize
import kotlinx.parcelize.RawValue
import kotlinx.serialization.Serializable
interface ComplexParameter<T : Parcelable>
@Serializable
object Dashboard
@Serializable
object AppSelector
@Serializable
data class InstalledApplicationInfo(val packageName: String)
@Serializable
data class Update(val downloadOnScreenEntry: Boolean = false)
@Serializable
data object SelectedApplicationInfo : ComplexParameter<SelectedApplicationInfo.ViewModelParams> {
@Parcelize
data class ViewModelParams(
val app: SelectedApp,
val patches: PatchSelection? = null
) : Parcelable
@Serializable
object Main
@Serializable
data object PatchesSelector : ComplexParameter<PatchesSelector.ViewModelParams> {
@Parcelize
data class ViewModelParams(
val app: SelectedApp,
val currentSelection: PatchSelection?,
val options: @RawValue Options,
) : Parcelable
}
@Serializable
data object RequiredOptions : ComplexParameter<PatchesSelector.ViewModelParams>
}
@Serializable
data object Patcher : ComplexParameter<Patcher.ViewModelParams> {
@Parcelize
data class ViewModelParams(
val selectedApp: SelectedApp,
val selectedPatches: PatchSelection,
val options: @RawValue Options
) : Parcelable
}
@Serializable
object Settings {
sealed interface Destination
@Serializable
data object Main : Destination
@Serializable
data object General : Destination
@Serializable
data object Advanced : Destination
@Serializable
data object Updates : Destination
@Serializable
data object Downloads : Destination
@Serializable
data object ImportExport : Destination
@Serializable
data object About : Destination
@Serializable
data object Changelogs : Destination
@Serializable
data object Contributors : Destination
@Serializable
data object Licenses : Destination
@Serializable
data object DeveloperOptions : Destination
}

View File

@ -25,7 +25,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.room.apps.installed.InstalledApp
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.isDefault
import app.revanced.manager.patcher.aapt.Aapt
import app.revanced.manager.ui.component.AlertDialogExtended
@ -60,7 +59,7 @@ fun DashboardScreen(
onSettingsClick: () -> Unit,
onUpdateClick: () -> Unit,
onDownloaderPluginClick: () -> Unit,
onAppClick: (InstalledApp) -> Unit
onAppClick: (String) -> Unit
) {
val bundlesSelectable by remember { derivedStateOf { vm.selectedSources.size > 0 } }
val availablePatches by vm.availablePatches.collectAsStateWithLifecycle(0)
@ -110,7 +109,6 @@ fun DashboardScreen(
)
}
val context = LocalContext.current
var showAndroid11Dialog by rememberSaveable { mutableStateOf(false) }
val installAppsPermissionLauncher =
rememberLauncherForActivityResult(RequestInstallAppsContract) { granted ->
@ -122,7 +120,7 @@ fun DashboardScreen(
showAndroid11Dialog = false
},
onContinue = {
installAppsPermissionLauncher.launch(context.packageName)
installAppsPermissionLauncher.launch(androidContext.packageName)
}
)
@ -240,6 +238,7 @@ fun DashboardScreen(
}
}
val showBatteryOptimizationsWarning by vm.showBatteryOptimizationsWarningFlow.collectAsStateWithLifecycle(false)
Notifications(
if (!Aapt.supportsDevice()) {
{
@ -251,7 +250,7 @@ fun DashboardScreen(
)
}
} else null,
if (vm.showBatteryOptimizationsWarning) {
if (showBatteryOptimizationsWarning) {
{
NotificationCard(
isWarning = true,
@ -289,7 +288,7 @@ fun DashboardScreen(
when (DashboardPage.entries[index]) {
DashboardPage.DASHBOARD -> {
InstalledAppsScreen(
onAppClick = onAppClick
onAppClick = { onAppClick(it.currentPackageName) }
)
}

View File

@ -77,10 +77,12 @@ fun InstalledAppInfoScreen(
.fillMaxSize()
.padding(paddingValues)
) {
AppInfo(viewModel.appInfo) {
Text(viewModel.installedApp.version, color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.bodyMedium)
val installedApp = viewModel.installedApp ?: return@ColumnWithScrollbar
if (viewModel.installedApp.installType == InstallType.MOUNT) {
AppInfo(viewModel.appInfo) {
Text(installedApp.version, color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.bodyMedium)
if (installedApp.installType == InstallType.MOUNT) {
Text(
text = if (viewModel.isMounted) {
stringResource(R.string.mounted)
@ -104,7 +106,7 @@ fun InstalledAppInfoScreen(
onClick = viewModel::launch
)
when (viewModel.installedApp.installType) {
when (installedApp.installType) {
InstallType.DEFAULT -> SegmentedButton(
icon = Icons.Outlined.Delete,
text = stringResource(R.string.uninstall),
@ -133,9 +135,9 @@ fun InstalledAppInfoScreen(
icon = Icons.Outlined.Update,
text = stringResource(R.string.repatch),
onClick = {
onPatchClick(viewModel.installedApp.originalPackageName)
onPatchClick(installedApp.originalPackageName)
},
enabled = viewModel.installedApp.installType != InstallType.MOUNT || viewModel.rootInstaller.hasRootAccess()
enabled = installedApp.installType != InstallType.MOUNT || viewModel.rootInstaller.hasRootAccess()
)
}
@ -158,19 +160,19 @@ fun InstalledAppInfoScreen(
SettingsListItem(
headlineContent = stringResource(R.string.package_name),
supportingContent = viewModel.installedApp.currentPackageName
supportingContent = installedApp.currentPackageName
)
if (viewModel.installedApp.originalPackageName != viewModel.installedApp.currentPackageName) {
if (installedApp.originalPackageName != installedApp.currentPackageName) {
SettingsListItem(
headlineContent = stringResource(R.string.original_package_name),
supportingContent = viewModel.installedApp.originalPackageName
supportingContent = installedApp.originalPackageName
)
}
SettingsListItem(
headlineContent = stringResource(R.string.install_type),
supportingContent = stringResource(viewModel.installedApp.installType.stringResource)
supportingContent = stringResource(installedApp.installType.stringResource)
)
}
}

View File

@ -1,16 +1,50 @@
package app.revanced.manager.ui.screen
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.EaseInOut
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
import androidx.compose.material.icons.outlined.*
import androidx.compose.material3.*
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.outlined.FilterList
import androidx.compose.material.icons.outlined.Restore
import androidx.compose.material.icons.outlined.Save
import androidx.compose.material.icons.outlined.Settings
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.IconButton
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Scaffold
import androidx.compose.material3.ScrollableTabRow
import androidx.compose.material3.SmallFloatingActionButton
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
@ -19,8 +53,10 @@ import androidx.compose.runtime.remember
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.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
@ -31,24 +67,24 @@ 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.LazyColumnWithScrollbar
import app.revanced.manager.ui.component.SafeguardDialog
import app.revanced.manager.ui.component.SearchView
import app.revanced.manager.ui.component.SearchBar
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.viewmodel.PatchesSelectorViewModel
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_SUPPORTED
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_UNIVERSAL
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_UNSUPPORTED
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_UNIVERSAL
import app.revanced.manager.util.Options
import app.revanced.manager.util.PatchSelection
import app.revanced.manager.util.isScrollingUp
import app.revanced.manager.util.transparentListItemColors
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
@Composable
fun PatchesSelectorScreen(
onSave: (PatchSelection?, Options) -> Unit,
@ -63,20 +99,17 @@ fun PatchesSelectorScreen(
bundles.size
}
val composableScope = rememberCoroutineScope()
var search: String? by rememberSaveable {
mutableStateOf(null)
val (query, setQuery) = rememberSaveable {
mutableStateOf("")
}
val (searchExpanded, setSearchExpanded) = rememberSaveable {
mutableStateOf(false)
}
var showBottomSheet by rememberSaveable { mutableStateOf(false) }
val showPatchButton by remember {
val showSaveButton by remember {
derivedStateOf { vm.selectionIsValid(bundles) }
}
val availablePatchCount by remember {
derivedStateOf {
bundles.sumOf { it.patchCount }
}
}
val defaultPatchSelectionCount by vm.defaultSelectionCount
.collectAsStateWithLifecycle(initialValue = 0)
@ -108,27 +141,22 @@ fun PatchesSelectorScreen(
style = MaterialTheme.typography.titleMedium
)
Row(
FlowRow(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(5.dp)
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
FilterChip(
selected = vm.filter and SHOW_SUPPORTED != 0,
onClick = { vm.toggleFlag(SHOW_SUPPORTED) },
CheckedFilterChip(
selected = vm.filter and SHOW_UNSUPPORTED == 0,
onClick = { vm.toggleFlag(SHOW_UNSUPPORTED) },
label = { Text(stringResource(R.string.supported)) }
)
FilterChip(
CheckedFilterChip(
selected = vm.filter and SHOW_UNIVERSAL != 0,
onClick = { vm.toggleFlag(SHOW_UNIVERSAL) },
label = { Text(stringResource(R.string.universal)) },
)
FilterChip(
selected = vm.filter and SHOW_UNSUPPORTED != 0,
onClick = { vm.toggleFlag(SHOW_UNSUPPORTED) },
label = { Text(stringResource(R.string.unsupported)) },
)
}
}
}
@ -175,20 +203,21 @@ fun PatchesSelectorScreen(
fun LazyListScope.patchList(
uid: Int,
patches: List<PatchInfo>,
filterFlag: Int,
visible: Boolean,
supported: Boolean,
header: (@Composable () -> Unit)? = null
) {
if (patches.isNotEmpty() && (vm.filter and filterFlag) != 0 || vm.filter == 0) {
if (patches.isNotEmpty() && visible) {
header?.let {
item {
item(contentType = 0) {
it()
}
}
items(
items = patches,
key = { it.name }
key = { it.name },
contentType = { 1 }
) { patch ->
PatchItem(
patch = patch,
@ -222,103 +251,142 @@ fun PatchesSelectorScreen(
}
}
search?.let { query ->
SearchView(
query = query,
onQueryChange = { search = it },
onActiveChange = { if (!it) search = null },
placeholder = { Text(stringResource(R.string.search_patches)) }
) {
val bundle = bundles[pagerState.currentPage]
LazyColumnWithScrollbar(
modifier = Modifier.fillMaxSize()
) {
fun List<PatchInfo>.searched() = filter {
it.name.contains(query, true)
}
patchList(
uid = bundle.uid,
patches = bundle.supported.searched(),
filterFlag = SHOW_SUPPORTED,
supported = true
)
patchList(
uid = bundle.uid,
patches = bundle.universal.searched(),
filterFlag = SHOW_UNIVERSAL,
supported = true
) {
ListHeader(
title = stringResource(R.string.universal_patches),
Scaffold(
topBar = {
SearchBar(
query = query,
onQueryChange = setQuery,
expanded = searchExpanded,
onExpandedChange = setSearchExpanded,
placeholder = {
Text(stringResource(R.string.search_patches))
},
leadingIcon = {
val rotation by animateFloatAsState(
targetValue = if (searchExpanded) 360f else 0f,
animationSpec = tween(durationMillis = 400, easing = EaseInOut),
label = "SearchBar back button"
)
IconButton(
onClick = {
if (searchExpanded) {
setSearchExpanded(false)
} else {
onBackClick()
}
}
) {
Icon(
modifier = Modifier.rotate(rotation),
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(R.string.back)
)
}
},
trailingIcon = {
AnimatedContent(
targetState = searchExpanded,
label = "Filter/Clear",
transitionSpec = { fadeIn() togetherWith fadeOut() }
) { searchExpanded ->
if (searchExpanded) {
IconButton(
onClick = { setQuery("") },
enabled = query.isNotEmpty()
) {
Icon(
imageVector = Icons.Filled.Close,
contentDescription = stringResource(R.string.clear)
)
}
} else {
IconButton(onClick = { showBottomSheet = true }) {
Icon(
imageVector = Icons.Outlined.FilterList,
contentDescription = stringResource(R.string.more)
)
}
}
}
}
) {
val bundle = bundles[pagerState.currentPage]
if (!vm.allowIncompatiblePatches) return@LazyColumnWithScrollbar
patchList(
uid = bundle.uid,
patches = bundle.unsupported.searched(),
filterFlag = SHOW_UNSUPPORTED,
supported = true
LazyColumnWithScrollbar(
modifier = Modifier.fillMaxSize()
) {
ListHeader(
title = stringResource(R.string.unsupported_patches),
onHelpClick = { showUnsupportedPatchesDialog = true }
fun List<PatchInfo>.searched() = filter {
it.name.contains(query, true)
}
patchList(
uid = bundle.uid,
patches = bundle.supported.searched(),
visible = true,
supported = true
)
patchList(
uid = bundle.uid,
patches = bundle.universal.searched(),
visible = vm.filter and SHOW_UNIVERSAL != 0,
supported = true
) {
ListHeader(
title = stringResource(R.string.universal_patches),
)
}
patchList(
uid = bundle.uid,
patches = bundle.unsupported.searched(),
visible = vm.filter and SHOW_UNSUPPORTED != 0,
supported = vm.allowIncompatiblePatches
) {
ListHeader(
title = stringResource(R.string.unsupported_patches),
onHelpClick = { showUnsupportedPatchesDialog = true }
)
}
}
}
},
floatingActionButton = {
if (!showSaveButton) return@Scaffold
AnimatedVisibility(
visible = !searchExpanded,
enter = slideInHorizontally { it } + fadeIn(),
exit = slideOutHorizontally { it } + fadeOut()
) {
Column(
horizontalAlignment = Alignment.End,
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
SmallFloatingActionButton(
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)) },
icon = {
Icon(
imageVector = Icons.Outlined.Save,
contentDescription = stringResource(R.string.save)
)
},
expanded = patchLazyListStates.getOrNull(pagerState.currentPage)?.isScrollingUp ?: true,
onClick = {
onSave(vm.getCustomSelection(), vm.getOptions())
}
)
}
}
}
}
Scaffold(
topBar = {
AppTopBar(
title = stringResource(
R.string.patches_selected,
selectedPatchCount,
availablePatchCount
),
onBackClick = onBackClick,
actions = {
IconButton(onClick = vm::reset) {
Icon(Icons.Outlined.Restore, stringResource(R.string.reset))
}
IconButton(onClick = { showBottomSheet = true }) {
Icon(Icons.Outlined.FilterList, stringResource(R.string.more))
}
IconButton(
onClick = {
search = ""
}
) {
Icon(Icons.Outlined.Search, stringResource(R.string.search))
}
}
)
},
floatingActionButton = {
if (!showPatchButton) return@Scaffold
HapticExtendedFloatingActionButton(
text = { Text(stringResource(R.string.save)) },
icon = {
Icon(
Icons.Outlined.Save,
stringResource(R.string.save)
)
},
expanded = patchLazyListStates.getOrNull(pagerState.currentPage)?.isScrollingUp
?: true,
onClick = {
// TODO: only allow this if all required options have been set.
onSave(vm.getCustomSelection(), vm.getOptions())
}
)
}
) { paddingValues ->
Column(
Modifier
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
@ -360,13 +428,13 @@ fun PatchesSelectorScreen(
patchList(
uid = bundle.uid,
patches = bundle.supported,
filterFlag = SHOW_SUPPORTED,
visible = true,
supported = true
)
patchList(
uid = bundle.uid,
patches = bundle.universal,
filterFlag = SHOW_UNIVERSAL,
visible = vm.filter and SHOW_UNIVERSAL != 0,
supported = true
) {
ListHeader(
@ -376,7 +444,7 @@ fun PatchesSelectorScreen(
patchList(
uid = bundle.uid,
patches = bundle.unsupported,
filterFlag = SHOW_UNSUPPORTED,
visible = vm.filter and SHOW_UNSUPPORTED != 0,
supported = vm.allowIncompatiblePatches
) {
ListHeader(
@ -464,7 +532,7 @@ private fun PatchItem(
)
@Composable
private fun ListHeader(
fun ListHeader(
title: String,
onHelpClick: (() -> Unit)? = null
) {

View File

@ -0,0 +1,158 @@
package app.revanced.manager.ui.screen
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AutoFixHigh
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.ScrollableTabRow
import androidx.compose.material3.Text
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
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.patcher.patch.Option
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
import app.revanced.manager.util.isScrollingUp
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun RequiredOptionsScreen(
onContinue: (PatchSelection?, Options) -> Unit,
onBackClick: () -> Unit,
vm: PatchesSelectorViewModel
) {
val list by vm.requiredOptsPatches.collectAsStateWithLifecycle(emptyList())
val pagerState = rememberPagerState(
initialPage = 0,
initialPageOffsetFraction = 0f
) {
list.size
}
val patchLazyListStates = remember(list) { List(list.size, ::LazyListState) }
val bundles by vm.bundlesFlow.collectAsStateWithLifecycle(emptyList())
val showContinueButton by remember {
derivedStateOf {
bundles.requiredOptionsSet(
isSelected = { bundle, patch -> vm.isSelected(bundle.uid, patch) },
optionsForPatch = { bundle, patch -> vm.getOptions(bundle.uid, patch) }
)
}
}
val composableScope = rememberCoroutineScope()
Scaffold(
topBar = {
AppTopBar(
title = stringResource(R.string.required_options_screen),
onBackClick = onBackClick
)
},
floatingActionButton = {
if (!showContinueButton) return@Scaffold
HapticExtendedFloatingActionButton(
text = { Text(stringResource(R.string.patch)) },
icon = {
Icon(
Icons.Default.AutoFixHigh,
stringResource(R.string.patch)
)
},
expanded = patchLazyListStates.getOrNull(pagerState.currentPage)?.isScrollingUp
?: true,
onClick = {
onContinue(vm.getCustomSelection(), vm.getOptions())
}
)
}
) { paddingValues ->
Column(
Modifier
.fillMaxSize()
.padding(paddingValues)
) {
if (list.isEmpty()) return@Column
else if (list.size > 1) ScrollableTabRow(
selectedTabIndex = pagerState.currentPage,
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp)
) {
list.forEachIndexed { index, (bundle, _) ->
HapticTab(
selected = pagerState.currentPage == index,
onClick = {
composableScope.launch {
pagerState.animateScrollToPage(
index
)
}
},
text = { Text(bundle.name) },
selectedContentColor = MaterialTheme.colorScheme.primary,
unselectedContentColor = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
HorizontalPager(
state = pagerState,
userScrollEnabled = true,
pageContent = { index ->
// Avoid crashing if the lists have not been fully initialized yet.
if (index > list.lastIndex || list.size != patchLazyListStates.size) return@HorizontalPager
val (bundle, patches) = list[index]
LazyColumnWithScrollbar(
modifier = Modifier.fillMaxSize(),
state = patchLazyListStates[index]
) {
items(patches, key = { it.name }) {
ListHeader(it.name)
val values = vm.getOptions(bundle.uid, it)
it.options?.forEach { option ->
val key = option.key
val value =
if (values == null || key !in values) option.default else values[key]
@Suppress("UNCHECKED_CAST")
OptionItem(
option = option as Option<Any>,
value = value,
setValue = { new ->
vm.setOption(bundle.uid, it, key, new)
}
)
}
}
}
}
)
}
}
}

View File

@ -14,9 +14,9 @@ import androidx.compose.material.icons.automirrored.outlined.ArrowRight
import androidx.compose.material.icons.filled.AutoFixHigh
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
@ -32,10 +32,7 @@ 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.haptics.HapticExtendedFloatingActionButton
import app.revanced.manager.ui.destination.SelectedAppInfoDestination
import app.revanced.manager.ui.model.BundleInfo.Extensions.bundleInfoFlow
import app.revanced.manager.ui.model.SelectedApp
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel
import app.revanced.manager.ui.viewmodel.SelectedAppInfoViewModel
import app.revanced.manager.util.EventEffect
import app.revanced.manager.util.Options
@ -43,14 +40,14 @@ import app.revanced.manager.util.PatchSelection
import app.revanced.manager.util.enabled
import app.revanced.manager.util.toast
import app.revanced.manager.util.transparentListItemColors
import dev.olshevski.navigation.reimagined.*
import org.koin.androidx.compose.koinViewModel
import org.koin.core.parameter.parametersOf
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SelectedAppInfoScreen(
onPatchClick: (SelectedApp, PatchSelection, Options) -> Unit,
onPatchSelectorClick: (SelectedApp, PatchSelection?, Options) -> Unit,
onRequiredOptions: (SelectedApp, PatchSelection?, Options) -> Unit,
onPatchClick: () -> Unit,
onBackClick: () -> Unit,
vm: SelectedAppInfoViewModel
) {
@ -58,20 +55,14 @@ fun SelectedAppInfoScreen(
val packageName = vm.selectedApp.packageName
val version = vm.selectedApp.version
val bundles by remember(packageName, version) {
vm.bundlesRepo.bundleInfoFlow(packageName, version)
}.collectAsStateWithLifecycle(initialValue = emptyList())
val bundles by vm.bundleInfoFlow.collectAsStateWithLifecycle(emptyList())
val allowIncompatiblePatches by vm.prefs.disablePatchVersionCompatCheck.getAsState()
val patches by remember {
derivedStateOf {
vm.getPatches(bundles, allowIncompatiblePatches)
}
val patches = remember(bundles, allowIncompatiblePatches) {
vm.getPatches(bundles, allowIncompatiblePatches)
}
val selectedPatchCount by remember {
derivedStateOf {
patches.values.sumOf { it.size }
}
val selectedPatchCount = remember(patches) {
patches.values.sumOf { it.size }
}
val launcher = rememberLauncherForActivityResult(
@ -81,147 +72,128 @@ fun SelectedAppInfoScreen(
EventEffect(flow = vm.launchActivityFlow) { intent ->
launcher.launch(intent)
}
val composableScope = rememberCoroutineScope()
val navController =
rememberNavController<SelectedAppInfoDestination>(startDestination = SelectedAppInfoDestination.Main)
val error by vm.errorFlow.collectAsStateWithLifecycle(null)
Scaffold(
topBar = {
AppTopBar(
title = stringResource(R.string.app_info),
onBackClick = onBackClick
)
},
floatingActionButton = {
if (error != null) return@Scaffold
NavBackHandler(controller = navController)
AnimatedNavHost(controller = navController) { destination ->
val error by vm.errorFlow.collectAsStateWithLifecycle(null)
when (destination) {
is SelectedAppInfoDestination.Main -> Scaffold(
topBar = {
AppTopBar(
title = stringResource(R.string.app_info),
onBackClick = onBackClick
HapticExtendedFloatingActionButton(
text = { Text(stringResource(R.string.patch)) },
icon = {
Icon(
Icons.Default.AutoFixHigh,
stringResource(R.string.patch)
)
},
floatingActionButton = {
if (error != null) return@Scaffold
onClick = patchClick@{
if (selectedPatchCount == 0) {
context.toast(context.getString(R.string.no_patches_selected))
HapticExtendedFloatingActionButton(
text = { Text(stringResource(R.string.patch)) },
icon = {
Icon(
Icons.Default.AutoFixHigh,
stringResource(R.string.patch)
)
},
onClick = patchClick@{
if (selectedPatchCount == 0) {
context.toast(context.getString(R.string.no_patches_selected))
return@patchClick
}
return@patchClick
}
onPatchClick(
composableScope.launch {
if (!vm.hasSetRequiredOptions(patches)) {
onRequiredOptions(
vm.selectedApp,
patches,
vm.getOptionsFiltered(bundles)
vm.getCustomPatches(bundles, allowIncompatiblePatches),
vm.options
)
return@launch
}
)
}
) { paddingValues ->
val plugins by vm.plugins.collectAsStateWithLifecycle(emptyList())
if (vm.showSourceSelector) {
val requiredVersion by vm.requiredVersion.collectAsStateWithLifecycle(null)
AppSourceSelectorDialog(
plugins = plugins,
installedApp = vm.installedAppData,
searchApp = SelectedApp.Search(
vm.packageName,
vm.desiredVersion
),
activeSearchJob = vm.activePluginAction,
hasRoot = vm.hasRoot,
onDismissRequest = vm::dismissSourceSelector,
onSelectPlugin = vm::searchUsingPlugin,
requiredVersion = requiredVersion,
onSelect = {
vm.selectedApp = it
vm.dismissSourceSelector()
}
)
}
ColumnWithScrollbar(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
AppInfo(vm.selectedAppInfo, placeholderLabel = packageName) {
Text(
version ?: stringResource(R.string.selected_app_meta_any_version),
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodyMedium,
)
}
PageItem(
R.string.patch_selector_item,
stringResource(
R.string.patch_selector_item_description,
selectedPatchCount
),
onClick = {
navController.navigate(
SelectedAppInfoDestination.PatchesSelector(
vm.selectedApp,
vm.getCustomPatches(
bundles,
allowIncompatiblePatches
),
vm.options
)
)
}
)
PageItem(
R.string.apk_source_selector_item,
when (val app = vm.selectedApp) {
is SelectedApp.Search -> stringResource(R.string.apk_source_auto)
is SelectedApp.Installed -> stringResource(R.string.apk_source_installed)
is SelectedApp.Download -> stringResource(
R.string.apk_source_downloader,
plugins.find { it.packageName == app.data.pluginPackageName }?.name
?: app.data.pluginPackageName
)
is SelectedApp.Local -> stringResource(R.string.apk_source_local)
},
onClick = {
vm.showSourceSelector()
}
)
error?.let {
Text(
stringResource(it.resourceId),
color = MaterialTheme.colorScheme.error,
modifier = Modifier.padding(horizontal = 24.dp)
)
onPatchClick()
}
}
)
}
) { paddingValues ->
val plugins by vm.plugins.collectAsStateWithLifecycle(emptyList())
if (vm.showSourceSelector) {
val requiredVersion by vm.requiredVersion.collectAsStateWithLifecycle(null)
AppSourceSelectorDialog(
plugins = plugins,
installedApp = vm.installedAppData,
searchApp = SelectedApp.Search(
vm.packageName,
vm.desiredVersion
),
activeSearchJob = vm.activePluginAction,
hasRoot = vm.hasRoot,
onDismissRequest = vm::dismissSourceSelector,
onSelectPlugin = vm::searchUsingPlugin,
requiredVersion = requiredVersion,
onSelect = {
vm.selectedApp = it
vm.dismissSourceSelector()
}
)
}
ColumnWithScrollbar(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
AppInfo(vm.selectedAppInfo, placeholderLabel = packageName) {
Text(
version ?: stringResource(R.string.selected_app_meta_any_version),
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodyMedium,
)
}
is SelectedAppInfoDestination.PatchesSelector -> PatchesSelectorScreen(
onSave = { patches, options ->
vm.updateConfiguration(patches, options, bundles)
navController.pop()
},
onBackClick = navController::pop,
vm = koinViewModel {
parametersOf(
PatchesSelectorViewModel.Params(
destination.app,
destination.currentSelection,
destination.options,
)
PageItem(
R.string.patch_selector_item,
stringResource(
R.string.patch_selector_item_description,
selectedPatchCount
),
onClick = {
onPatchSelectorClick(
vm.selectedApp,
vm.getCustomPatches(
bundles,
allowIncompatiblePatches
),
vm.options
)
}
)
PageItem(
R.string.apk_source_selector_item,
when (val app = vm.selectedApp) {
is SelectedApp.Search -> stringResource(R.string.apk_source_auto)
is SelectedApp.Installed -> stringResource(R.string.apk_source_installed)
is SelectedApp.Download -> stringResource(
R.string.apk_source_downloader,
plugins.find { it.packageName == app.data.pluginPackageName }?.name
?: app.data.pluginPackageName
)
is SelectedApp.Local -> stringResource(R.string.apk_source_local)
},
onClick = {
vm.showSourceSelector()
}
)
error?.let {
Text(
stringResource(it.resourceId),
color = MaterialTheme.colorScheme.error,
modifier = Modifier.padding(horizontal = 24.dp)
)
}
}
}
}

View File

@ -13,146 +13,64 @@ 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.settings.SettingsListItem
import app.revanced.manager.ui.destination.SettingsDestination
import app.revanced.manager.ui.screen.settings.*
import app.revanced.manager.ui.screen.settings.update.ChangelogsScreen
import app.revanced.manager.ui.screen.settings.update.UpdateScreen
import app.revanced.manager.ui.screen.settings.update.UpdatesSettingsScreen
import app.revanced.manager.ui.viewmodel.SettingsViewModel
import dev.olshevski.navigation.reimagined.*
import org.koin.androidx.compose.koinViewModel
import org.koin.core.parameter.parametersOf
import app.revanced.manager.ui.model.navigation.Settings
private val settingsSections = listOf(
Triple(
R.string.general,
R.string.general_description,
Icons.Outlined.Settings
) to Settings.General,
Triple(
R.string.updates,
R.string.updates_description,
Icons.Outlined.Update
) to Settings.Updates,
Triple(
R.string.downloads,
R.string.downloads_description,
Icons.Outlined.Download
) to Settings.Downloads,
Triple(
R.string.import_export,
R.string.import_export_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,
startDestination: SettingsDestination,
viewModel: SettingsViewModel = koinViewModel()
) {
val navController = rememberNavController(startDestination)
val backClick: () -> Unit = {
if (navController.backstack.entries.size == 1)
onBackClick()
else navController.pop()
}
val settingsSections = listOf(
Triple(
R.string.general,
R.string.general_description,
Icons.Outlined.Settings
) to SettingsDestination.General,
Triple(
R.string.updates,
R.string.updates_description,
Icons.Outlined.Update
) to SettingsDestination.Updates,
Triple(
R.string.downloads,
R.string.downloads_description,
Icons.Outlined.Download
) to SettingsDestination.Downloads,
Triple(
R.string.import_export,
R.string.import_export_description,
Icons.Outlined.SwapVert
) to SettingsDestination.ImportExport,
Triple(
R.string.advanced,
R.string.advanced_description,
Icons.Outlined.Tune
) to SettingsDestination.Advanced,
Triple(
R.string.about,
R.string.app_name,
Icons.Outlined.Info
) to SettingsDestination.About,
)
NavBackHandler(navController)
AnimatedNavHost(
controller = navController
) { destination ->
when (destination) {
is SettingsDestination.General -> GeneralSettingsScreen(
onBackClick = backClick,
viewModel = viewModel
fun SettingsScreen(onBackClick: () -> Unit, navigate: (Settings.Destination) -> Unit) {
Scaffold(
topBar = {
AppTopBar(
title = stringResource(R.string.settings),
onBackClick = onBackClick,
)
is SettingsDestination.Advanced -> AdvancedSettingsScreen(
onBackClick = backClick
)
is SettingsDestination.Updates -> UpdatesSettingsScreen(
onBackClick = backClick,
onChangelogClick = { navController.navigate(SettingsDestination.Changelogs) },
onUpdateClick = { navController.navigate(SettingsDestination.Update(false)) }
)
is SettingsDestination.Downloads -> DownloadsSettingsScreen(
onBackClick = backClick
)
is SettingsDestination.ImportExport -> ImportExportSettingsScreen(
onBackClick = backClick
)
is SettingsDestination.About -> AboutSettingsScreen(
onBackClick = backClick,
onContributorsClick = { navController.navigate(SettingsDestination.Contributors) },
onDeveloperOptionsClick = { navController.navigate(SettingsDestination.DeveloperOptions) },
onLicensesClick = { navController.navigate(SettingsDestination.Licenses) },
)
is SettingsDestination.Update -> UpdateScreen(
onBackClick = backClick,
vm = koinViewModel {
parametersOf(
destination.downloadOnScreenEntry
)
}
)
is SettingsDestination.Changelogs -> ChangelogsScreen(
onBackClick = backClick,
)
is SettingsDestination.Contributors -> ContributorScreen(
onBackClick = backClick,
)
is SettingsDestination.Licenses -> LicensesScreen(
onBackClick = backClick,
)
is SettingsDestination.DeveloperOptions -> DeveloperOptionsScreen(onBackClick = backClick)
is SettingsDestination.Settings -> {
Scaffold(
topBar = {
AppTopBar(
title = stringResource(R.string.settings),
onBackClick = backClick,
)
}
) { paddingValues ->
ColumnWithScrollbar(
modifier = Modifier
.padding(paddingValues)
.fillMaxSize()
) {
settingsSections.forEach { (titleDescIcon, destination) ->
SettingsListItem(
modifier = Modifier.clickable { navController.navigate(destination) },
headlineContent = stringResource(titleDescIcon.first),
supportingContent = stringResource(titleDescIcon.second),
leadingContent = { Icon(titleDescIcon.third, null) }
)
}
}
}
}
) { paddingValues ->
ColumnWithScrollbar(
modifier = Modifier
.padding(paddingValues)
.fillMaxSize()
) {
settingsSections.forEach { (titleDescIcon, destination) ->
SettingsListItem(
modifier = Modifier.clickable { navigate(destination) },
headlineContent = stringResource(titleDescIcon.first),
supportingContent = stringResource(titleDescIcon.second),
leadingContent = { Icon(titleDescIcon.third, null) }
)
}
}
}

View File

@ -1,4 +1,4 @@
package app.revanced.manager.ui.screen.settings.update
package app.revanced.manager.ui.screen
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.spring

View File

@ -37,6 +37,7 @@ import app.revanced.manager.network.dto.ReVancedSocial
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 app.revanced.manager.ui.viewmodel.AboutViewModel
import app.revanced.manager.ui.viewmodel.AboutViewModel.Companion.getSocialIcon
import app.revanced.manager.util.openUrl
@ -47,9 +48,7 @@ import org.koin.androidx.compose.koinViewModel
@Composable
fun AboutSettingsScreen(
onBackClick: () -> Unit,
onContributorsClick: () -> Unit,
onLicensesClick: () -> Unit,
onDeveloperOptionsClick: () -> Unit,
navigate: (Settings.Destination) -> Unit,
viewModel: AboutViewModel = koinViewModel()
) {
val context = LocalContext.current
@ -114,17 +113,17 @@ fun AboutSettingsScreen(
Triple(
stringResource(R.string.contributors),
stringResource(R.string.contributors_description),
third = onContributorsClick
third = { navigate(Settings.Contributors) }
),
Triple(
stringResource(R.string.developer_options),
stringResource(R.string.developer_options_description),
third = onDeveloperOptionsClick
third = { navigate(Settings.DeveloperOptions) }
),
Triple(
stringResource(R.string.opensource_licenses),
stringResource(R.string.opensource_licenses_description),
third = onLicensesClick
third = { navigate(Settings.Licenses) }
)
)

View File

@ -31,6 +31,7 @@ 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.IntegerItem
import app.revanced.manager.ui.component.settings.SafeguardBooleanItem
import app.revanced.manager.ui.component.settings.SettingsListItem
import app.revanced.manager.ui.viewmodel.AdvancedSettingsViewModel
import app.revanced.manager.util.toast
@ -104,29 +105,33 @@ fun AdvancedSettingsScreen(
)
GroupHeader(stringResource(R.string.safeguards))
BooleanItem(
SafeguardBooleanItem(
preference = vm.prefs.disablePatchVersionCompatCheck,
coroutineScope = vm.viewModelScope,
headline = R.string.patch_compat_check,
description = R.string.patch_compat_check_description
description = R.string.patch_compat_check_description,
confirmationText = R.string.patch_compat_check_confirmation
)
BooleanItem(
SafeguardBooleanItem(
preference = vm.prefs.disableUniversalPatchWarning,
coroutineScope = vm.viewModelScope,
headline = R.string.universal_patches_safeguard,
description = R.string.universal_patches_safeguard_description
description = R.string.universal_patches_safeguard_description,
confirmationText = R.string.universal_patches_safeguard_confirmation
)
BooleanItem(
SafeguardBooleanItem(
preference = vm.prefs.suggestedVersionSafeguard,
coroutineScope = vm.viewModelScope,
headline = R.string.suggested_version_safeguard,
description = R.string.suggested_version_safeguard_description
description = R.string.suggested_version_safeguard_description,
confirmationText = R.string.suggested_version_safeguard_confirmation
)
BooleanItem(
SafeguardBooleanItem(
preference = vm.prefs.disableSelectionWarning,
coroutineScope = vm.viewModelScope,
headline = R.string.patch_selection_safeguard,
description = R.string.patch_selection_safeguard_description
description = R.string.patch_selection_safeguard_description,
confirmationText = R.string.patch_selection_safeguard_confirmation
)
GroupHeader(stringResource(R.string.debugging))

View File

@ -1,6 +1,5 @@
package app.revanced.manager.ui.screen.settings
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@ -96,7 +95,7 @@ fun ContributorScreen(
}
}
@OptIn(ExperimentalLayoutApi::class, ExperimentalFoundationApi::class)
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun ContributorsCard(
title: String,
@ -131,7 +130,7 @@ fun ContributorsCard(
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = processHeadlineText(title),
text = title,
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Medium)
)
Text(
@ -199,11 +198,4 @@ fun ContributorsCard(
}
}
}
}
fun processHeadlineText(repositoryName: String): String {
return "ReVanced " + repositoryName.replace("revanced/revanced-", "")
.replace("-", " ")
.split(" ").joinToString(" ") { if (it.length > 3) it else it.uppercase() }
.replaceFirstChar { it.uppercase() }
}

View File

@ -10,7 +10,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
@ -32,14 +31,15 @@ import app.revanced.manager.ui.component.haptics.HapticRadioButton
import app.revanced.manager.ui.component.settings.BooleanItem
import app.revanced.manager.ui.component.settings.SettingsListItem
import app.revanced.manager.ui.theme.Theme
import app.revanced.manager.ui.viewmodel.SettingsViewModel
import app.revanced.manager.ui.viewmodel.GeneralSettingsViewModel
import org.koin.androidx.compose.koinViewModel
import org.koin.compose.koinInject
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun GeneralSettingsScreen(
onBackClick: () -> Unit,
viewModel: SettingsViewModel
viewModel: GeneralSettingsViewModel = koinViewModel()
) {
val prefs = viewModel.prefs
val coroutineScope = viewModel.viewModelScope

View File

@ -24,7 +24,9 @@ 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.delay
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
@ -56,14 +58,19 @@ class DashboardViewModel(
var updatedManagerVersion: String? by mutableStateOf(null)
private set
var showBatteryOptimizationsWarning by mutableStateOf(false)
private set
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()
showBatteryOptimizationsWarning =
!powerManager.isIgnoringBatteryOptimizations(app.packageName)
}
}

View File

@ -6,7 +6,7 @@ import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.ui.theme.Theme
import kotlinx.coroutines.launch
class SettingsViewModel(
class GeneralSettingsViewModel(
val prefs: PreferencesManager
) : ViewModel() {
fun setTheme(theme: Theme) = viewModelScope.launch {

View File

@ -32,15 +32,17 @@ import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class InstalledAppInfoViewModel(
val installedApp: InstalledApp
packageName: String
) : ViewModel(), KoinComponent {
private val app: Application by inject()
private val context: Application by inject()
private val pm: PM by inject()
private val installedAppRepository: InstalledAppRepository by inject()
val rootInstaller: RootInstaller by inject()
lateinit var onBackClick: () -> Unit
var installedApp: InstalledApp? by mutableStateOf(null)
private set
var appInfo: PackageInfo? by mutableStateOf(null)
private set
var appliedPatches: PatchSelection? by mutableStateOf(null)
@ -49,38 +51,48 @@ class InstalledAppInfoViewModel(
init {
viewModelScope.launch {
isMounted = rootInstaller.isAppMounted(installedApp.currentPackageName)
installedApp = installedAppRepository.get(packageName)?.also {
isMounted = rootInstaller.isAppMounted(it.currentPackageName)
appInfo = withContext(Dispatchers.IO) {
pm.getPackageInfo(it.currentPackageName)
}
appliedPatches = withContext(Dispatchers.IO) {
installedAppRepository.getAppliedPatches(it.currentPackageName)
}
}
}
}
fun launch() = pm.launch(installedApp.currentPackageName)
fun launch() = installedApp?.currentPackageName?.let(pm::launch)
fun mountOrUnmount() = viewModelScope.launch {
val pkgName = installedApp?.currentPackageName ?: return@launch
try {
if (isMounted)
rootInstaller.unmount(installedApp.currentPackageName)
rootInstaller.unmount(pkgName)
else
rootInstaller.mount(installedApp.currentPackageName)
rootInstaller.mount(pkgName)
} catch (e: Exception) {
if (isMounted) {
app.toast(app.getString(R.string.failed_to_unmount, e.simpleMessage()))
context.toast(context.getString(R.string.failed_to_unmount, e.simpleMessage()))
Log.e(tag, "Failed to unmount", e)
} else {
app.toast(app.getString(R.string.failed_to_mount, e.simpleMessage()))
context.toast(context.getString(R.string.failed_to_mount, e.simpleMessage()))
Log.e(tag, "Failed to mount", e)
}
} finally {
isMounted = rootInstaller.isAppMounted(installedApp.currentPackageName)
isMounted = rootInstaller.isAppMounted(pkgName)
}
}
fun uninstall() {
when (installedApp.installType) {
InstallType.DEFAULT -> pm.uninstallPackage(installedApp.currentPackageName)
val app = installedApp ?: return
when (app.installType) {
InstallType.DEFAULT -> pm.uninstallPackage(app.currentPackageName)
InstallType.MOUNT -> viewModelScope.launch {
rootInstaller.uninstall(installedApp.currentPackageName)
installedAppRepository.delete(installedApp)
rootInstaller.uninstall(app.currentPackageName)
installedAppRepository.delete(app)
onBackClick()
}
}
@ -97,34 +109,22 @@ class InstalledAppInfoViewModel(
if (extraStatus == PackageInstaller.STATUS_SUCCESS) {
viewModelScope.launch {
installedAppRepository.delete(installedApp)
installedApp?.let {
installedAppRepository.delete(it)
}
onBackClick()
}
} else if (extraStatus != PackageInstaller.STATUS_FAILURE_ABORTED) {
app.toast(app.getString(R.string.uninstall_app_fail, extraStatusMessage))
this@InstalledAppInfoViewModel.context.toast(this@InstalledAppInfoViewModel.context.getString(R.string.uninstall_app_fail, extraStatusMessage))
}
}
}
}
}
init {
viewModelScope.launch {
appInfo = withContext(Dispatchers.IO) {
pm.getPackageInfo(installedApp.currentPackageName)
}
}
viewModelScope.launch {
appliedPatches = withContext(Dispatchers.IO) {
installedAppRepository.getAppliedPatches(installedApp.currentPackageName)
}
}
}.also {
ContextCompat.registerReceiver(
app,
uninstallBroadcastReceiver,
context,
it,
IntentFilter(UninstallService.APP_UNINSTALL_ACTION),
ContextCompat.RECEIVER_NOT_EXPORTED
)
@ -132,6 +132,6 @@ class InstalledAppInfoViewModel(
override fun onCleared() {
super.onCleared()
app.unregisterReceiver(uninstallBroadcastReceiver)
context.unregisterReceiver(uninstallBroadcastReceiver)
}
}

View File

@ -2,13 +2,10 @@ package app.revanced.manager.ui.viewmodel
import android.app.Activity
import android.app.Application
import android.content.ActivityNotFoundException
import android.content.Intent
import android.util.Base64
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import app.revanced.manager.R
@ -28,6 +25,7 @@ import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.Json
class MainViewModel(
@ -36,10 +34,13 @@ class MainViewModel(
private val downloadedAppRepository: DownloadedAppRepository,
private val keystoreManager: KeystoreManager,
private val app: Application,
val prefs: PreferencesManager
val prefs: PreferencesManager,
private val json: Json
) : ViewModel() {
private val appSelectChannel = Channel<SelectedApp>()
val appSelectFlow = appSelectChannel.receiveAsFlow()
private val legacyImportActivityChannel = Channel<Intent>()
val legacyImportActivityFlow = legacyImportActivityChannel.receiveAsFlow()
private suspend fun suggestedVersion(packageName: String) =
patchBundleRepository.suggestedVersions.first()[packageName]
@ -50,7 +51,8 @@ class MainViewModel(
val suggestedVersion = suggestedVersion(app.packageName) ?: return null
val downloadedApp =
downloadedAppRepository.get(app.packageName, suggestedVersion, markUsed = true) ?: return null
downloadedAppRepository.get(app.packageName, suggestedVersion, markUsed = true)
?: return null
return SelectedApp.Local(
downloadedApp.packageName,
downloadedApp.version,
@ -67,42 +69,46 @@ class MainViewModel(
selectApp(SelectedApp.Search(packageName, suggestedVersion(packageName)))
}
fun importLegacySettings(componentActivity: ComponentActivity) {
if (!prefs.firstLaunch.getBlocking()) return
try {
val launcher = componentActivity.registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result: ActivityResult ->
if (result.resultCode == Activity.RESULT_OK) {
result.data?.getStringExtra("data")?.let {
applyLegacySettings(it)
} ?: app.toast(app.getString(R.string.legacy_import_failed))
} else {
app.toast(app.getString(R.string.legacy_import_failed))
}
}
val intent = Intent().apply {
init {
viewModelScope.launch {
if (!prefs.firstLaunch.get()) return@launch
legacyImportActivityChannel.send(Intent().apply {
setClassName(
"app.revanced.manager.flutter",
"app.revanced.manager.flutter.ExportSettingsActivity"
)
}
launcher.launch(intent)
} catch (e: Exception) {
if (e !is ActivityNotFoundException) {
app.toast(app.getString(R.string.legacy_import_failed))
Log.e(tag, "Failed to launch legacy import activity: $e")
}
})
}
}
private fun applyLegacySettings(data: String) = viewModelScope.launch {
val json = Json { ignoreUnknownKeys = true }
val settings = json.decodeFromString<LegacySettings>(data)
fun applyLegacySettings(result: ActivityResult) {
if (result.resultCode != Activity.RESULT_OK) {
app.toast(app.getString(R.string.legacy_import_failed))
Log.e(
tag,
"Got unknown result code while importing legacy settings: ${result.resultCode}"
)
return
}
val jsonStr = result.data?.getStringExtra("data")
if (jsonStr == null) {
app.toast(app.getString(R.string.legacy_import_failed))
Log.e(tag, "Legacy settings data is null")
return
}
val settings = try {
json.decodeFromString<LegacySettings>(jsonStr)
} catch (e: SerializationException) {
app.toast(app.getString(R.string.legacy_import_failed))
Log.e(tag, "Legacy settings data could not be deserialized", e)
return
}
applyLegacySettings(settings)
}
private fun applyLegacySettings(settings: LegacySettings) = viewModelScope.launch {
settings.themeMode?.let { theme ->
val themeMap = mapOf(
0 to Theme.SYSTEM,
@ -145,6 +151,7 @@ class MainViewModel(
settings.patches?.let { selection ->
patchSelectionRepository.import(0, selection)
}
Log.d(tag, "Imported legacy settings")
}
@Serializable

View File

@ -39,7 +39,6 @@ import app.revanced.manager.plugin.downloader.PluginHostApi
import app.revanced.manager.plugin.downloader.UserInteractionException
import app.revanced.manager.service.InstallService
import app.revanced.manager.service.UninstallService
import app.revanced.manager.ui.destination.Destination
import app.revanced.manager.ui.model.InstallerModel
import app.revanced.manager.ui.model.ProgressKey
import app.revanced.manager.ui.model.SelectedApp
@ -47,6 +46,7 @@ import app.revanced.manager.ui.model.State
import app.revanced.manager.ui.model.Step
import app.revanced.manager.ui.model.StepCategory
import app.revanced.manager.ui.model.StepProgressProvider
import app.revanced.manager.ui.model.navigation.Patcher
import app.revanced.manager.util.PM
import app.revanced.manager.util.saveableVar
import app.revanced.manager.util.saver.snapshotStateListSaver
@ -72,7 +72,7 @@ import java.time.Duration
@OptIn(SavedStateHandleSaveableApi::class, PluginHostApi::class)
class PatcherViewModel(
private val input: Destination.Patcher
private val input: Patcher.ViewModelParams
) : ViewModel(), KoinComponent, StepProgressProvider, InstallerModel {
private val app: Application by inject()
private val fs: Filesystem by inject()

View File

@ -1,7 +1,6 @@
package app.revanced.manager.ui.viewmodel
import android.app.Application
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.setValue
@ -22,7 +21,7 @@ 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.SelectedApp
import app.revanced.manager.ui.model.navigation.SelectedApplicationInfo
import app.revanced.manager.util.Options
import app.revanced.manager.util.PatchSelection
import app.revanced.manager.util.saver.Nullable
@ -36,11 +35,14 @@ 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
@Stable
@OptIn(SavedStateHandleSaveableApi::class)
class PatchesSelectorViewModel(input: Params) : ViewModel(), KoinComponent {
class PatchesSelectorViewModel(input: SelectedApplicationInfo.PatchesSelector.ViewModelParams) :
ViewModel(), KoinComponent {
private val app: Application = get()
private val savedStateHandle: SavedStateHandle = get()
private val prefs: PreferencesManager = get()
@ -101,7 +103,7 @@ class PatchesSelectorViewModel(input: Params) : ViewModel(), KoinComponent {
val compatibleVersions = mutableStateListOf<String>()
var filter by mutableIntStateOf(0)
var filter by mutableIntStateOf(SHOW_UNIVERSAL)
private set
private val defaultPatchSelection = bundlesFlow.map { bundles ->
@ -113,6 +115,22 @@ class PatchesSelectorViewModel(input: Params) : ViewModel(), KoinComponent {
selection.values.sumOf { it.size }
}
// This is for the required options screen.
private val requiredOptsPatchesDeferred = viewModelScope.async(start = CoroutineStart.LAZY) {
bundlesFlow.first().map { bundle ->
bundle to bundle.all.filter { patch ->
val opts by lazy {
getOptions(bundle.uid, patch).orEmpty()
}
isSelected(
bundle.uid,
patch
) && patch.options?.any { it.required && it.default == null && it.key !in opts } ?: false
}.toList()
}.filter { (_, patches) -> patches.isNotEmpty() }
}
val requiredOptsPatches = flow { emit(requiredOptsPatchesDeferred.await()) }
fun selectionIsValid(bundles: List<BundleInfo>) = bundles.any { bundle ->
bundle.patchSequence(allowIncompatiblePatches).any { patch ->
isSelected(bundle.uid, patch)
@ -199,9 +217,8 @@ class PatchesSelectorViewModel(input: Params) : ViewModel(), KoinComponent {
}
companion object {
const val SHOW_SUPPORTED = 1 // 2^0
const val SHOW_UNSUPPORTED = 1 // 2^0
const val SHOW_UNIVERSAL = 2 // 2^1
const val SHOW_UNSUPPORTED = 4 // 2^2
private val optionsSaver: Saver<PersistentOptions, Options> = snapshotStateMapSaver(
// Patch name -> Options
@ -214,12 +231,6 @@ class PatchesSelectorViewModel(input: Params) : ViewModel(), KoinComponent {
private val selectionSaver: Saver<PersistentPatchSelection?, Nullable<PatchSelection>> =
nullableSaver(persistentMapSaver(valueSaver = persistentSetSaver()))
}
data class Params(
val app: SelectedApp,
val currentSelection: PatchSelection?,
val options: Options,
)
}
// Versions of other types, but utilizing persistent/observable collection types.

View File

@ -9,6 +9,7 @@ import android.util.Log
import androidx.activity.result.ActivityResult
import androidx.annotation.StringRes
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
@ -29,12 +30,17 @@ import app.revanced.manager.domain.repository.PatchOptionsRepository
import app.revanced.manager.domain.repository.PatchSelectionRepository
import app.revanced.manager.network.downloader.LoadedDownloaderPlugin
import app.revanced.manager.network.downloader.ParceledDownloaderData
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
import app.revanced.manager.util.Options
import app.revanced.manager.util.PM
import app.revanced.manager.util.PatchSelection
@ -57,9 +63,10 @@ import org.koin.core.component.KoinComponent
import org.koin.core.component.get
@OptIn(SavedStateHandleSaveableApi::class, PluginHostApi::class)
class SelectedAppInfoViewModel(input: Params) : ViewModel(), KoinComponent {
class SelectedAppInfoViewModel(
input: SelectedApplicationInfo.ViewModelParams
) : ViewModel(), KoinComponent {
private val app: Application = get()
val bundlesRepo: PatchBundleRepository = get()
private val bundleRepository: PatchBundleRepository = get()
private val selectionRepository: PatchSelectionRepository = get()
private val optionsRepository: PatchOptionsRepository = get()
@ -110,7 +117,10 @@ class SelectedAppInfoViewModel(input: Params) : ViewModel(), KoinComponent {
}
}
val requiredVersion = combine(prefs.suggestedVersionSafeguard.flow, bundleRepository.suggestedVersions) { suggestedVersionSafeguard, suggestedVersions ->
val requiredVersion = combine(
prefs.suggestedVersionSafeguard.flow,
bundleRepository.suggestedVersions
) { suggestedVersionSafeguard, suggestedVersions ->
if (!suggestedVersionSafeguard) return@combine null
suggestedVersions[input.app.packageName]
@ -167,6 +177,10 @@ class SelectedAppInfoViewModel(input: Params) : ViewModel(), KoinComponent {
}
}
val bundleInfoFlow by derivedStateOf {
bundleRepository.bundleInfoFlow(packageName, selectedApp.version)
}
fun showSourceSelector() {
dismissSourceSelector()
showSourceSelector = true
@ -253,6 +267,23 @@ class SelectedAppInfoViewModel(input: Params) : ViewModel(), KoinComponent {
selectedAppInfo = info
}
suspend fun hasSetRequiredOptions(patchSelection: PatchSelection) = bundleInfoFlow
.first()
.requiredOptionsSet(
isSelected = { bundle, patch -> patch.name in patchSelection[bundle.uid]!! },
optionsForPatch = { bundle, patch -> options[bundle.uid]?.get(patch.name) },
)
suspend fun getPatcherParams(): Patcher.ViewModelParams {
val allowUnsupported = prefs.disablePatchVersionCompatCheck.get()
val bundles = bundleInfoFlow.first()
return Patcher.ViewModelParams(
selectedApp,
getPatches(bundles, allowUnsupported),
getOptionsFiltered(bundles)
)
}
fun getOptionsFiltered(bundles: List<BundleInfo>) = options.filtered(bundles)
fun getPatches(bundles: List<BundleInfo>, allowUnsupported: Boolean) =
@ -264,17 +295,15 @@ class SelectedAppInfoViewModel(input: Params) : ViewModel(), KoinComponent {
): PatchSelection? =
(selectionState as? SelectionState.Customized)?.patches(bundles, allowUnsupported)
fun updateConfiguration(
selection: PatchSelection?,
options: Options,
bundles: List<BundleInfo>
) {
fun updateConfiguration(selection: PatchSelection?, options: Options) = viewModelScope.launch {
val bundles = bundleInfoFlow.first()
selectionState = selection?.let(SelectionState::Customized) ?: SelectionState.Default
val filteredOptions = options.filtered(bundles)
this.options = filteredOptions
this@SelectedAppInfoViewModel.options = filteredOptions
if (!persistConfiguration) return
if (!persistConfiguration) return@launch
viewModelScope.launch(Dispatchers.Default) {
selection?.let { selectionRepository.updateSelection(packageName, it) }
?: selectionRepository.clearSelection(packageName)
@ -283,11 +312,6 @@ class SelectedAppInfoViewModel(input: Params) : ViewModel(), KoinComponent {
}
}
data class Params(
val app: SelectedApp,
val patches: PatchSelection?,
)
enum class Error(@StringRes val resourceId: Int) {
NoPlugins(R.string.downloader_no_plugins_available)
}

View File

@ -3,11 +3,6 @@ package app.revanced.manager.util
import android.content.Context
import android.content.Intent
import android.content.pm.ApplicationInfo
import android.icu.number.Notation
import android.icu.number.NumberFormatter
import android.icu.number.Precision
import android.icu.text.CompactDecimalFormat
import android.os.Build
import android.util.Log
import android.widget.Toast
import androidx.annotation.MainThread

View File

@ -88,12 +88,16 @@
<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 supported 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</string>
<string name="patch_selection_safeguard_description">Do not prevent selecting or deselecting patches</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">Disable universal patch warning</string>
<string name="universal_patches_safeguard_description">Disables the warning that appears when you try to select 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>
@ -142,6 +146,8 @@
<string name="options">Options</string>
<string name="ok">OK</string>
<string name="yes">Yes</string>
<string name="no">No</string>
<string name="edit">Edit</string>
<string name="dialog_input_placeholder">Value</string>
<string name="reset">Reset</string>
@ -158,6 +164,7 @@
<string name="warning">Warning</string>
<string name="add">Add</string>
<string name="close">Close</string>
<string name="clear">Clear</string>
<string name="system">System</string>
<string name="light">Light</string>
<string name="dark">Dark</string>
@ -204,7 +211,7 @@
<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="bundles_selected">%s selected</string>
<string name="unsupported_patches">Unsupported patches</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_options_reset_toast">Patch options have been reset</string>
@ -213,10 +220,10 @@
<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\nThis warning can be disabled in the advanced settings.</string>
<string name="supported">Supported</string>
<string name="universal">Universal</string>
<string name="supported">This version</string>
<string name="universal">Any app</string>
<string name="unsupported">Unsupported</string>
<string name="search_patches">Patch name</string>
<string name="search_patches">Search patches</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_supported">Not all patches support this version (%s). Do you want to continue anyway?</string>
@ -348,6 +355,7 @@
<string name="download_manager_failed">Failed to download update: %s</string>
<string name="cancel">Cancel</string>
<string name="save">Save</string>
<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. \n ReVanced Manager will close when updating.</string>
@ -359,6 +367,8 @@
<string name="invalid_date">Invalid date</string>
<string name="disable_battery_optimization">Disable battery optimization</string>
<string name="input_dialog_value_invalid">Invalid value</string>
<string name="option_required">This option is required</string>
<string name="required_options_screen">Required options</string>
<string name="failed_to_check_updates">Failed to check for updates: %s</string>
<string name="no_update_available">No update available</string>

View File

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -1,10 +1,11 @@
# 💼 Prerequisites
In order to use ReVanced Manager, certain requirements must be met.
To use ReVanced Manager, certain requirements have to be met.
## 🤝 Requirements
- An Android device running Android 8 or higher
- Android device running Android 8.0 or higher
- **Optionally** Rooted Android device with latest version of [KernelSU](https://github.com/tiann/KernelSU) or [Magisk](https://github.com/topjohnwu/Magisk)
## ⏭️ What's next

View File

@ -1,14 +1,26 @@
# ⬇️ Installation
In order to use ReVanced on your Android device, ReVanced Manager must be installed.
To use ReVanced on your Android device, ReVanced Manager have to be install,
refer to the [Prerequisites](0_prerequisites.md) if haven't already.
## Installation steps
## 🪜 Installation steps
1. Download the latest version of ReVanced Manager from [here](https://github.com/revanced/revanced-manager/releases/latest)
1. Get the latest version of ReVanced Manager from the [ReVanced site][Official ReVanced Download].
2. Install ReVanced Manager
### ✒️ Verifying authenticity of ReVanced Manager
> [!NOTE]
> It's always advisable that you download from trusted sources like the [ReVanced site][Official ReVanced Download]
> or GitHub releases as it's the safest way to download ReVanced without potential risk of malware.
To verify if your ReVanced Manager is provided without sign of tampering,
we have signed the APK with hashes corresponding to `b6362c6ea7888efd15c0800f480786ad0f5b133b4f84e12d46afba5f9eac1223` *unless otherwise stated*.
## ⏭️ What's next
The next page will guide you through using ReVanced Manager.
Continue: [🛠️ Usage](2_usage.md)
[Official ReVanced Download]: https://revanced.app/download

View File

@ -8,16 +8,26 @@ The following pages will guide you through using ReVanced Manager to patch apps.
2. Tap the + button in the bottom right corner
3. Choose an app to patch[^1]
4. Tap on the version of the app you want to patch[^2]
5. Select the patches you want to apply
6. Tap the Patch button
7. Tap on the **Install** button
> **Note**: If you are rooted, you can mount the patched app on top of the original app.[^3]
> Optionally, you may export the patched app to storage using the options in the top right corner.
5. Tap the 🪄 **Patch** button
6. Tap on the 📲 **Install** button
> [!Note]
> If the **Install** Button isn't there, it could be that something went wrong. Refer to [3. ❔ Troubleshooting](3_troubleshooting.md)
> [!Tip]
> If you are rooted, you can mount the patched app on top of the original app.[^3]
> Optionally, you may export the patched app to storage using the options in the bottom left corner.
[^1]: Non-root users may be prompted to select an APK from storage, in which case you have to source the APK file yourself. ReVanced does not provide any APK files.
[^1]: You need to select an APK from storage, in which case you have to source the APK file yourself. ReVanced does not provide any APK files.
[^2]: It is suggested to use the version with the most patches to get the most out of ReVanced.
[^3]: Mounting the patched app on top of the original app will only work if the installed app version matches the version of the app selected in step 4. above.
### Change patch selection and options
Before you can change the selection of patches, follow the step below to allow changing the selection of patches.
1. Tap ⚙️ Settings button in the top right corner
2. Go to Advanced Settings
3. Toggle "Allow changing patch selection" on
## ⏭️ What's next
The next page will bring you back to the usage page.

View File

@ -6,10 +6,13 @@ After patching an app, you may want to manage it. This page will guide you throu
1. Navigate to the Apps tab from the top navigation bar
2. Select the app you want to manage
3.
Inside you will be able to open, and uninstall the app within the manager,
additionally you will be able to repatch the app by patching the app again, see [🧩 Patching apps](2_1_patching.md),
and view all patches applied to the patched app.
## ⏭️ What's next
The next page will bring you back to the usage page.
Continue: [🛠️ Usage](2_usage.md)

View File

@ -4,7 +4,25 @@ In order to keep up with the latest features and bug fixes, it is recommended to
## ✅ Updating steps
> Currently not implemented
1. Tap the ↻ Update button in the top right corner right next to the ⚙️ Settings icon
2. Tap the **Update** button in the bottom right corner
3. Wait for the update to be downloaded
4. Tap the **Install** button to install[^1]
> [!Note]
> If you see a prompt regarding your device is unable to install application for security reason
> you may need to allow ReVanced Manager to install APK file on your device.
> 1. Tap on Settings
> 2. Toggle "Allow from this source" on
5. Wait for installation prompt to show then tap **Install** again
6. Open ReVanced Manager again to see the new version
## 🔎 Check for update
Usually ReVanced Manager always check update on first startup, so it's not need to manually check for update yourself.
To trigger the check for update function follow the step bellow:
1. Go to ⚙️ Settings menu and ↻ Update section
2. Tap on "Check for updates"
## ⏭️ What's next

View File

@ -8,30 +8,44 @@ ReVanced Manager has settings that can be configured to your liking.
Specify the URL of the API to use. This is used to fetch ReVanced Patches and update ReVanced Manager.
Location: `⚙️ Settings -> Advanced -> API URL`
- ### 🧬 Sources
Override the API and change the source of ReVanced Patches.
- ### 🧪 Experimental ReVanced Patches support
Location: `Patch bundles -> + Button`
- ### 🧪 Experimental patches
Lift app version constraints from ReVanced Patches. This allows you to patch any version of an app, even if the patch is not explicitly compatible with it.
Location: `⚙️ Settings -> Advanced -> Safeguards`
- ### 🧑‍🔬 Experimental universal support
This will show or hide ReVanced Patches, which are not meant for any app in particular but rather for all apps but may not work on all apps.
Location: `⚙️ Settings -> Advanced -> Safeguards`
- ### 🔑 Export, import or delete keystore
Manage the keystore used to sign patched apps.
- ### 📄 Export, import or reset ReVanced Patches selection
Location: `⚙️ Settings -> Import & export -> Signing`
Manage the ReVanced Patches selection. This is useful if you want to share your ReVanced Patches selection with others or reset it to the default selection.
- ### 📄 Export, import or reset patch selection
Manage the ReVanced Patches selection. This is useful if you want to share your patch selection with others or reset it to the default selection.
Location: `⚙️ Settings -> Import & export -> Patches`
- ### About
View information about your device and ReVanced Manager. This includes the version of ReVanced Manager and supported architectures of your device.
Location: `⚙️ Settings -> About`
## ⏭️ What's next
The next page will bring you back to the usage page.

View File

@ -10,11 +10,19 @@ In case you encounter any issues while using ReVanced Manager, please refer to t
An existing installation of the app you're trying to patch is conflicting with the patched app. Uninstall the existing app before installing the patched app.
- 🥛 Out Of Memory error
This indicates that your device is not giving ReVanced Manager enough memory to patch the app.
Turning on `⚙️ Settings -> Advanced -> toggle "Run Patcher in another process (experimental)"` may solve the issue.
- ❗️ Error code `135`, `139` or `1` when patching the app
Your device is not supported. Refer to the [Prerequisites](0_prerequisites.md) page for supported devices.
Alternatively, you can use [ReVanced CLI](https://github.com/revanced/revanced-cli) to patch the app.
Alternatively, you can use [ReVanced CLI][ReVanced CLI GitHub]) to patch the app.
[ReVanced CLI GitHub]: https://github.com/revanced/revanced-cli
- 🚫 Non-root install is not possible with the current patches selection
@ -22,10 +30,14 @@ In case you encounter any issues while using ReVanced Manager, please refer to t
- 🚨 Patched app crashes on launch
Select the **Default** button when choosing patches.
Try selecting the **Default** button when choosing patches, if the patched app still failed, file an issue at [ReVanced Patches][ReVanced Patches GitHub].
[ReVanced Patches GitHub]: https://github.com/revanced/revanced-patches
## ⏭️ What's next
The next page will teach you how to build ReVanced Manager from source.
You have successfully finished the guide for on how to use ReVanced Manager.
Continue: [🔨 Building from source](4_building.md)
If you wish to review the Table of Content for how to use ReVanced Manager, feel free to do so.
Continue: [💊 ReVanced Manager](/docs/README.md)

View File

@ -1,38 +0,0 @@
# 🛠️ Building from source
This page will guide you through building ReVanced Manager from source.
1. Download Java SDK 17 ([Azul JDK](https://www.azul.com/downloads/?version=java-17-lts&package=jdk#zulu) or [OpenJDK](https://jdk.java.net/java-se-ri/17)) and add it to path
2. Clone the repository
```sh
git clone https://github.com/revanced/revanced-manager.git && cd revanced-manager
```
3. Create a GitHub personal access token with the `read:packages` scope [here](https://github.com/settings/tokens/new?scopes=read:packages&description=ReVanced)
4. Add your GitHub username and the token to `~/.gradle/gradle.properties`
```properties
gpr.user = YourUsername
gpr.key = ghp_longrandomkey
```
5. Set the `sdk.dir` property in `local.properties` to your Android SDK location
```properties
sdk.dir = /path/to/android/sdk
```
6. Build the APK
Debug:
```sh
./gradlew assembleDebug
```
Release:
```sh
./gradlew assembleRelease -Psign
```

View File

@ -1,6 +1,6 @@
# 💊 ReVanced Manager
This documentation explains how to use [ReVanced Manager](https://github.com/revanced/revanced-manager).
This documentation explains how to use [ReVanced Manager](https://github.com/ReVanced/revanced-manager).
## 📖 Table of contents
@ -12,7 +12,10 @@ This documentation explains how to use [ReVanced Manager](https://github.com/rev
3. [🔄 Updating ReVanced Manager](2_3_updating.md)
4. [⚙️ Configuring ReVanced Manager](2_4_settings.md)
3. [❔ Troubleshooting](3_troubleshooting.md)
4. [🔨 Building from source](4_building.md)
## 🧑‍💻 Developing for ReVanced Manager
Checkout the documentation for developer [here](developer/README.md).
## ⏭️ Start here

View File

@ -0,0 +1,28 @@
# 💼 Prerequisites
Here's everything you'll need to develop on ReVanced Manager
## 🤝 Recommended environment
- Any environment that is capable of running the latest Android Studio, latest Android SDK, and JDK 17 with at least 4 GB of memory for coding
- Any devices with preferably the latest Android version or at least higher than the [`minSdk`](/app/build.gradle.kts) for testing
- **Optionally** A device with root capabilities using the latest version of [KernelSU](https://github.com/tiann/KernelSU) or [Magisk](https://github.com/topjohnwu/Magisk)
## ⚙️ Setting up
### Authenticating to GitHub Registry
ReVanced Manager use dependency from the GitHub Registry (i.e., [ReVanced Patcher](https://github.com/ReVanced/revanced-patcher/packages)) and so your build may fail without authenticating to the service,
to authenticate you must create a personal access token with the scope `read:packages` [here](https://github.com/settings/tokens/new?scopes=read:packages&description=ReVanced)
and add your token to `~/.gradle/gradle.properties`, create the file if it does not exist.
```properties
gpr.user = username
gpr.key = ghp_******************************
```
## ⏭️ What's next
The next page will guide you through developing for ReVanced Manager.
Continue: [🧑‍💻 Developing for ReVanced Manager](1_develop.md)

View File

@ -0,0 +1,25 @@
# 🧑‍💻 Developing for ReVanced Manager
ReVanced Manager is developed on Kotlin using [Jetpack Compose](https://developer.android.com/compose), here are some tips to help you get started.
## Code style
The styling is based on https://kotlinlang.org/docs/coding-conventions.html
## Commit
At ReVanced, we adopt a naming convention for commit called [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) so for example when you're adding a new feature you must add `feat: ` before the description of your commit.
## Building
To build ReVanced Manager, simply hit build button on your IDE or run:
```sh
./gradlew assembleDebug
```
## ⏭️ What's next
The next page will guide you through developing for ReVanced Manager.
Continue: [🎉 Submitting your Pull Request](2_submitting.md)

View File

@ -0,0 +1,21 @@
# 🎉 Submitting your Pull Request
> [!TIP]
> We recommend that you discuss your changes with
> the maintainers of ReVanced Manager before contributing.
> This will help you determine whether your change is acceptable.
1. Fork the repository and create your branch from `dev`
2. Commit your changes in [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) style
3. 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
4. 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
## ⏭️ What's next
You have successfully finished the guide for developing for ReVanced Manager.
This next section will be verifying the authenticity of ReVanced Manager
which may be relevant for store provider or those who's looking to verify prebuilt librar(ies).
Continue: [✒️ Verification and Authenticity of ReVanced Manager](3_verifying.md)

View File

@ -0,0 +1,19 @@
# ✒️ Verification and Authenticity of ReVanced Manager
> [!NOTE]
> This information is more relevant to alternative store providers
To distribute ReVanced Manager safely and recommended way is to provide the users with a link to
[1. ⬇️ Installation, ✒️ Verifying authenticity of ReVanced Manager (user-side)][installation authenticity].
The certificate SHA-256 of the APK will always be `b6362c6ea7888efd15c0800f480786ad0f5b133b4f84e12d46afba5f9eac1223` unless otherwise noted.
[installation authenticity]: /docs/1_installation.md#%EF%B8%8F-verifying-authenticity-of-revanced-manager
## `libaapt2.so`
ReVanced Manager includes prebuilt binaries of [`libaapt2.so`][location of libaapt2.so] from https://github.com/ReVanced/aapt2
which fixes issues on ARM32-based systems. Attestation is provided here: https://github.com/ReVanced/aapt2/attestations
[location of libaapt2.so]: /app/src/main/jniLibs/

25
docs/developer/README.md Normal file
View File

@ -0,0 +1,25 @@
# 💊 ReVanced Manager
> [!WARNING]
> This guide is for people who want to develop for ReVanced Manager, may feature technical words or jargons.
> For regular user you may want to view this [documentation](/docs/README.md) instead.
This documentation explains how to develop for [ReVanced Manager](https://github.com/ReVanced/revanced-manager).
## 📖 Table of contents
## Developing
0. [💼 Prerequisites](0_prerequisites.md)
1. [🧑‍💻 Developing for ReVanced Manager](1_developing.md)
2. [🎉 Submitting your Pull Request](2_submitting.md)
## Transparency
1. [✒️ Verification and Authenticity of ReVanced Manager](3_verifying.md)
## ⏭️ Start here
The next page will tell you about the prerequisites for developing for ReVanced Manager.
Continue: [💼 Prerequisites](0_prerequisites.md)

View File

@ -9,6 +9,7 @@ appcompat = "1.7.0"
preferences-datastore = "1.1.1"
work-runtime = "2.10.0"
compose-bom = "2024.12.01"
navigation = "2.8.5"
accompanist = "0.34.0"
placeholder = "1.1.2"
reorderable = "1.5.2"
@ -18,14 +19,12 @@ datetime = "0.6.0"
room-version = "2.6.1"
revanced-patcher = "21.0.0"
revanced-library = "3.0.2"
koin-version = "3.5.3"
koin-version-compose = "3.5.3"
reimagined-navigation = "1.5.0"
koin = "3.5.3"
ktor = "2.3.9"
markdown-renderer = "0.22.0"
fading-edges = "1.0.4"
kotlin = "2.1.0"
android-gradle-plugin = "8.7.3"
android-gradle-plugin = "8.8.0"
dev-tools-gradle-plugin = "2.1.0-1.0.29"
about-libraries-gradle-plugin = "11.1.1"
binary-compatibility-validator = "0.17.0"
@ -57,8 +56,9 @@ compose-ui = { group = "androidx.compose.ui", name = "ui" }
compose-ui-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling", version.ref = "ui-tooling" }
compose-livedata = { group = "androidx.compose.runtime", name = "runtime-livedata" }
compose-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3"}
compose-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3" }
compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" }
navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigation" }
# Coil
coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil" }
@ -85,12 +85,10 @@ revanced-patcher = { group = "app.revanced", name = "revanced-patcher", version.
revanced-library = { group = "app.revanced", name = "revanced-library", version.ref = "revanced-library" }
# Koin
koin-android = { group = "io.insert-koin", name = "koin-android", version.ref = "koin-version" }
koin-compose = { group = "io.insert-koin", name = "koin-androidx-compose", version.ref = "koin-version-compose" }
koin-workmanager = { group = "io.insert-koin", name = "koin-androidx-workmanager", version.ref = "koin-version" }
# Compose Navigation
reimagined-navigation = { group = "dev.olshevski.navigation", name = "reimagined", version.ref = "reimagined-navigation" }
koin-android = { group = "io.insert-koin", name = "koin-android", version.ref = "koin" }
koin-compose = { group = "io.insert-koin", name = "koin-androidx-compose", version.ref = "koin" }
koin-compose-navigation = { group = "io.insert-koin", name = "koin-androidx-compose-navigation", version.ref = "koin" }
koin-workmanager = { group = "io.insert-koin", name = "koin-androidx-workmanager", version.ref = "koin" }
# About Libraries
about-libraries = { group = "com.mikepenz", name = "aboutlibraries-compose", version.ref = "about-libraries-gradle-plugin" }

Binary file not shown.

View File

@ -1,7 +1,7 @@
#Tue Nov 12 21:36:50 CET 2024
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
distributionSha256Sum=7a00d51fb93147819aab76024feece20b6b84e420694101f276be952e08bef03
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME

6
gradlew vendored
View File

@ -15,6 +15,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
@ -55,7 +57,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@ -84,7 +86,7 @@ done
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum

22
gradlew.bat vendored
View File

@ -13,6 +13,8 @@
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@ -43,11 +45,11 @@ set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
@ -57,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail