Compare commits

..

39 Commits

Author SHA1 Message Date
4f8fd23a87 use snackbar instead because toasts are dumb 2025-05-15 20:50:35 +02:00
98c0262d10 add strings 2025-05-15 19:39:38 +02:00
f412c99c60 rename 2025-05-15 19:10:49 +02:00
62d5863846 Merge branch 'compose-dev' into feat/hide-dev-options 2025-05-15 19:01:01 +02:00
e71e6fb797 fix: handle edge-to-edge properly in fullscreen dialogs 2025-05-14 20:23:23 +02:00
111e8808a0 feat(Compose): Add confirmation dialog on multiple operations (#2529) 2025-05-14 19:55:09 +02:00
a30ff45111 refactor: Rename settings screens for consistency (#2547) 2025-05-14 19:34:05 +02:00
eb03daaebd feat(Compose): Improve patches selector tab by adding the bundle version (#2545) 2025-05-14 19:33:05 +02:00
9e2a7917ba ci: Adjust and modernize workflow files to match other repos 2025-05-12 18:32:30 +02:00
88fc639751 docs: Adjust issue templates to match other repos 2025-05-12 18:32:30 +02:00
2fc099568e docs: Add contribution guidelines and adjust README 2025-05-12 18:32:29 +02:00
e641b91e57 build: Update Gradle Wrapper 2025-05-12 18:32:29 +02:00
d09e9b79cb fix: patch count remaining at zero when using process runtime (#2542) 2025-05-11 19:30:12 +02:00
3f5d04a513 feat: Order bundles by number of patches 2025-05-11 13:10:11 +02:00
3ad027007f style: Apply formatting 2025-05-10 23:07:46 +02:00
813ec4dbe2 build: Sign releases using keystore if available 2025-05-10 20:00:01 +02:00
9fac5126df feat: Use "Debug" and "Debug signed" for build names respectively 2025-05-10 20:00:00 +02:00
5e03cf2307 feat: Move safeguards above patcher preference group 2025-05-10 20:00:00 +02:00
4d522664b6 fix: Reset cached theme on theme change to avoid broken colors (#2527) 2025-05-08 15:31:34 +02:00
0058b5f504 feat: hide developer options 2025-05-07 18:57:11 +02:00
8fdd5074a0 feat(Compose): Move developer options to top level (#2528) 2025-05-07 18:37:30 +02:00
5555373deb build: Remove repos that are not required 2025-05-05 19:37:10 +02:00
6e57a6e977 fix: Ignore long click when already in delete mode
closes #2503
2025-04-30 13:34:33 +03:00
10001b492b feat: add network checks for features that require it 2025-04-28 18:10:01 +02:00
d37ed050bc feat: move plugin api to another repository 2025-04-28 17:19:07 +02:00
ee07621e37 fix: Do not poll battery optimization status (#2491) 2025-04-23 19:26:04 +02:00
fc05f95837 feat: Improve update screen design (#2487) 2025-04-23 20:09:05 +03:00
d5c63ead26 fix: Use compatible rather than support when referring to patch compatibility (#2422) 2025-02-13 00:40:47 +07:00
1956982060 feat: Improve APK file name formatting on save (#2421) 2025-02-13 00:40:36 +07:00
e10e5e4e3f build(deps): bump the gradle-compose group with 16 updates (#2407) 2025-02-01 10:45:14 +07:00
ede1ab5ed4 feat: Reorder Import & Export settings (#2403) 2025-02-01 04:05:28 +03:00
1092188ab0 feat: TopAppBar scroll behavior (#2397) 2025-01-31 15:03:50 +03:00
f348eba115 ci: Generate release artifact provenance (#2324)
Signed-off-by: validcube <pun.butrach@gmail.com>
2025-01-29 20:43:16 +01:00
3d234820a3 fix: improve keystore import error handling and show toast 2025-01-29 19:58:38 +01:00
cd06d36f68 build: Enable pseudo locale for debug variant 2025-01-29 23:22:24 +07:00
242c4570ce chore: Update project's dependencies to latest 2025-01-29 23:17:34 +07:00
71b73a3b42 fix: show install button when installation has been cancelled 2025-01-29 15:17:41 +01:00
067020f38f feat: Screen slide transition (#2396) 2025-01-27 19:28:55 +07:00
2aef67872d fix: Offset badge 2025-01-27 03:56:15 +03:00
112 changed files with 1610 additions and 2454 deletions

View File

@ -1,61 +0,0 @@
name: 🐞 Bug report
description: Create a new bug report.
title: 'bug: <title>'
labels: [bug]
body:
- type: markdown
attributes:
value: |
# ReVanced Manager bug report
Please check for existing issues [here](https://github.com/revanced/revanced-manager/labels/bug) before creating a new one.
- type: textarea
attributes:
label: Bug description
description: |
- Describe your bug in detail
- Add steps to reproduce the bug if possible (Step 1. Download some files. Step 2. ...)
- Add images and videos if possible
- List selected patches if applicable
validations:
required: true
- type: textarea
attributes:
label: Version of ReVanced Manager and version & name of application you tried to patch
validations:
required: true
- type: dropdown
attributes:
label: Installation type
options:
- Non-root
- Root
validations:
required: false
- type: textarea
attributes:
label: Device logs
description: Export logs in ReVanced Manager settings.
render: shell
validations:
required: true
- type: textarea
attributes:
label: Patcher logs
description: Export logs in "Patcher" screen.
render: shell
validations:
required: false
- type: checkboxes
attributes:
label: Acknowledgements
description: Your issue will be closed if you don't follow the checklist below!
options:
- label: This request is not a duplicate of an existing issue.
required: true
- label: I have chosen an appropriate title.
required: true
- label: All requested information has been provided properly.
required: true
- label: The issue is solely related to the ReVanced Manager
required: true

109
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@ -0,0 +1,109 @@
name: 🐞 Bug report
description: Report a bug or an issue.
title: 'bug: '
labels: ['Bug report']
body:
- type: markdown
attributes:
value: |
<p align="center">
<picture>
<source
width="256px"
media="(prefers-color-scheme: dark)"
srcset="https://raw.githubusercontent.com/revanced/revanced-manager/main/assets/revanced-headline/revanced-headline-vertical-dark.svg"
>
<img
width="256px"
src="https://raw.githubusercontent.com/revanced/revanced-manager/main/assets/revanced-headline/revanced-headline-vertical-light.svg"
>
</picture>
<br>
<a href="https://revanced.app/">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/revanced/revanced-manager/main/assets/revanced-logo/revanced-logo.svg" />
<img height="24px" src="https://raw.githubusercontent.com/revanced/revanced-manager/main/assets/revanced-logo/revanced-logo.svg" />
</picture>
</a>&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>
# ReVanced Manager bug report
Before creating a new bug report, please keep the following in mind:
- **Do not submit a duplicate bug report**: Search for existing bug reports [here](https://github.com/ReVanced/revanced-manager/issues?q=label%3A%22Bug+report%22).
- **Review the contribution guidelines**: Make sure your bug report adheres to it. You can find the guidelines [here](https://github.com/ReVanced/revanced-manager/blob/main/CONTRIBUTING.md).
- **Do not use the issue page for support**: If you need help or have questions, check out other platforms on [revanced.app](https://revanced.app).
- type: textarea
attributes:
label: Bug description
description: |
- Describe your bug in detail
- Add steps to reproduce the bug if possible (Step 1. ... Step 2. ...)
- Add images and videos if possible
- List used patches, downloader and settings if applicable
validations:
required: true
- type: textarea
attributes:
label: Patch logs
description: Patch logs can be exported by clicking on the "Logs" button in the "Patcher" screen, when patching finishes.
render: shell
- type: textarea
attributes:
label: Debug logs
description: Debug logs can be exported by clicking on "Export debug logs" in "Settings" > "Advanced".
validations:
required: true
- type: checkboxes
attributes:
label: Acknowledgements
description: Your bug report will be closed if you don't follow the checklist below.
options:
- label: I have checked all open and closed bug reports and this is not a duplicate.
required: true
- label: I have chosen an appropriate title.
required: true
- label: All requested information has been provided properly.
required: true
- label: The bug is only related to ReVanced Manager.
required: true

View File

@ -1 +1,5 @@
blank_issues_enabled: false blank_issues_enabled: false
contact_links:
- name: 🗨 Discussions
url: https://github.com/revanced/revanced-suggestions/discussions
about: Have something unspecific to ReVanced Manager in mind? Search for or start a new discussion!

View File

@ -1,42 +0,0 @@
name: ⭐ Feature request
description: Create a new feature request.
title: 'feat: <title>'
labels: [feature request]
body:
- type: markdown
attributes:
value: |
# ReVanced Manager feature request
Please check for existing feature requests [here](https://github.com/revanced/revanced-manager/labels/bug) before creating a new one.
- type: textarea
attributes:
label: Feature description
description: Describe your feature in detail.
validations:
required: true
- type: textarea
attributes:
label: Motivation
description: Explain why the lack of it is a problem.
validations:
required: true
- type: textarea
attributes:
label: Additional context
description: In case there is something else you want to add.
validations:
required: false
- type: checkboxes
attributes:
label: Acknowledgements
description: Your issue will be closed if you don't follow the checklist below!
options:
- label: This request is not a duplicate of an existing issue.
required: true
- label: I have chosen an appropriate title.
required: true
- label: All requested information has been provided properly.
required: true
- label: The issue is solely related to the ReVanced Manager
required: true

View File

@ -0,0 +1,103 @@
body:
- type: markdown
attributes:
value: |
<p align="center">
<picture>
<source
width="256px"
media="(prefers-color-scheme: dark)"
srcset="https://raw.githubusercontent.com/revanced/revanced-manager/main/assets/revanced-headline/revanced-headline-vertical-dark.svg"
>
<img
width="256px"
src="https://raw.githubusercontent.com/revanced/revanced-manager/main/assets/revanced-headline/revanced-headline-vertical-light.svg"
>
</picture>
<br>
<a href="https://revanced.app/">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/revanced/revanced-manager/main/assets/revanced-logo/revanced-logo.svg" />
<img height="24px" src="https://raw.githubusercontent.com/revanced/revanced-manager/main/assets/revanced-logo/revanced-logo.svg" />
</picture>
</a>&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>
# ReVanced Manager feature request
Before creating a new feature request, please keep the following in mind:
- **Do not submit a duplicate feature request**: Search for existing feature requests [here](https://github.com/ReVanced/revanced-manager/issues?q=label%3A%22Feature+request%22).
- **Review the contribution guidelines**: Make sure your feature request adheres to it. You can find the guidelines [here](https://github.com/ReVanced/revanced-manager/blob/main/CONTRIBUTING.md).
- **Do not use the issue page for support**: If you need help or have questions, check out other platforms on [revanced.app](https://revanced.app).
- type: textarea
attributes:
label: Feature description
description: |
- Describe your feature in detail
- Add images, videos, links, examples, references, etc. if possible
- type: textarea
attributes:
label: Motivation
description: |
A strong motivation is necessary for a feature request to be considered.
- Why should this feature be implemented?
- What is the explicit use case?
- What are the benefits?
- What makes this feature important?
validations:
required: true
- type: checkboxes
id: acknowledgements
attributes:
label: Acknowledgements
description: Your feature request will be closed if you don't follow the checklist below.
options:
- label: I have checked all open and closed feature requests and this is not a duplicate
required: true
- label: I have chosen an appropriate title.
required: true
- label: All requested information has been provided properly.
required: true
- label: The feature request is only related to ReVanced Manager.
required: true

2
.github/config.yaml vendored
View File

@ -1,2 +1,2 @@
firstPRMergeComment: > firstPRMergeComment: >
❤️ Thank you for contributing to ReVanced Manager. Join us on [Discord](https://revanced.app/discord) if you want to receive a contributor role. Thank you for contributing to ReVanced. Join us on [Discord](https://revanced.app/discord) to receive a role for your contribution.

View File

@ -0,0 +1,25 @@
name: Build pull request
on:
workflow_dispatch:
pull_request:
branches:
- dev
jobs:
release:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Cache Gradle
uses: burrunan/gradle-cache-action@v1
- name: Build
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: ./gradlew build --no-daemon

26
.github/workflows/open_pull_request.yml vendored Normal file
View File

@ -0,0 +1,26 @@
name: Open a PR to main
on:
push:
branches:
- dev
workflow_dispatch:
env:
MESSAGE: Merge branch `${{ github.head_ref || github.ref_name }}` to `main`
jobs:
pull-request:
name: Open pull request
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Open pull request
uses: repo-sync/pull-request@v2
with:
destination_branch: 'main'
pr_title: 'chore: ${{ env.MESSAGE }}'
pr_body: 'This pull request will ${{ env.MESSAGE }}.'
pr_draft: true

View File

@ -1,44 +0,0 @@
name: Build pull request
on:
pull_request:
paths:
- ".github/workflows/pr-build.yml"
- "app/**"
- "gradle/**"
- "*.properties"
- ".kts"
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
- name: Set up Gradle
uses: gradle/actions/setup-gradle@v4
- name: Build with Gradle
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: ./gradlew assembleRelease --no-daemon -PnoProguard -PsignAsDebug
- name: Set env
run: echo "COMMIT_HASH=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
- name: Add hash to APK
run: mv app/build/outputs/apk/release/app-release.apk revanced-manager-${{ env.COMMIT_HASH }}.apk
- name: Upload build
uses: actions/upload-artifact@v4
with:
name: revanced-manager
path: revanced-manager-${{ env.COMMIT_HASH }}.apk

View File

@ -1,49 +0,0 @@
name: Release Build
on:
push:
tags:
- "v*"
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set env
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
- name: Set up Gradle
uses: gradle/actions/setup-gradle@v4
- name: Build with Gradle
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: ./gradlew assembleRelease --no-daemon
- name: Sign APK
id: sign_apk
uses: ilharp/sign-android-release@v1
with:
releaseDir: ./app/build/outputs/apk/release/
signingKey: ${{ secrets.SIGNING_KEYSTORE }}
keyStorePassword: ${{ secrets.SIGNING_KEYSTORE_PASSWORD }}
keyAlias: ${{ secrets.SIGNING_KEY_ALIAS }}
keyPassword: ${{ secrets.SIGNING_KEY_PASSWORD }}
- name: Add version to APK
run: mv ${{ steps.sign_apk.outputs.signedFile }} revanced-manager-${{ env.RELEASE_VERSION }}.apk
- name: Publish release APK
uses: "marvinpinto/action-automatic-releases@latest"
with:
repo_token: "${{ secrets.GITHUB_TOKEN }}"
prerelease: false
files: revanced-manager-${{ env.RELEASE_VERSION }}.apk

64
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,64 @@
name: Release
on:
workflow_dispatch:
push:
branches:
- main
- dev
jobs:
release:
name: Release
permissions:
contents: write
id-token: write
attestations: write
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
- name: Cache Gradle
uses: burrunan/gradle-cache-action@v1
- name: Build
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: ./gradlew assembleRelease
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "lts/*"
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Setup keystore
run: |
echo "${{ secrets.KEYSTORE }}" | base64 --decode > "app/keystore.jks"
- name: Semantic Release
uses: cycjimmy/semantic-release-action@v4
id: semantic
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
KEYSTORE_ENTRY_ALIAS: ${{ secrets.KEYSTORE_ENTRY_ALIAS }}
KEYSTORE_ENTRY_PASSWORD: ${{ secrets.KEYSTORE_ENTRY_PASSWORD }}
- name: Attest
if: steps.semantic.outputs.new_release_published == 'true'
uses: actions/attest-build-provenance@v2
with:
subject-path: build/app/outputs/apk/release/revanced-manager-*.apk

View File

@ -16,4 +16,4 @@ jobs:
token: ${{ secrets.DOCUMENTATION_REPO_ACCESS_TOKEN }} token: ${{ secrets.DOCUMENTATION_REPO_ACCESS_TOKEN }}
repository: revanced/revanced-documentation repository: revanced/revanced-documentation
event-type: update-documentation event-type: update-documentation
client-payload: '{"repo": "${{ github.event.repository.name }}", "ref": "${{ github.ref }}"}' client-payload: '{"repo": "${{ github.event.repository.name }}", "ref": "${{ github.ref }}"}'

View File

@ -60,52 +60,44 @@
# 👋 Contribution guidelines # 👋 Contribution guidelines
Welcome to contribution guidelines, this document contains This document describes how to contribute to ReVanced Manager.
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 ## 📖 Resources to help you get started
* The [documentation](/docs/developer/README.md) provides steps to build ReVanced Manager from source * The [documentation](/docs/README.md) provides steps to build ReVanced Manager from source
* Our [backlog](https://github.com/orgs/ReVanced/projects/12) is where we keep track of what we're working on * 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 * [Issues](https://github.com/ReVanced/revanced-manager/issues) are where we keep track of bugs and feature requests
## 🙏 Submitting a feature request ## 🙏 Submitting a feature request
Features can be requested by opening an issue using the 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). [Feature request issue template](https://github.com/ReVanced/revanced-manager/issues/new?assignees=&labels=Feature+request&projects=&template=feature_request.yml&title=feat%3A+).
> [!NOTE] > **Note**
> Requests can be accepted or rejected at the discretion of maintainers of ReVanced Manager. > 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. > Good motivation has to be provided for a request to be accepted.
## 🐞 Submitting a bug report ## 🐞 Submitting a bug report
If you encounter a bug while using the ReVanced Manager app, open an issue using the If you encounter a bug while using ReVanced Manager, open an issue using the
[bug report issue template](https://github.com/ReVanced/revanced-manager/issues/new?assignees=&labels=bug&projects=&template=bug-issue.yml&title=bug%3A+%3Ctitle%3E). [Bug report issue template](https://github.com/ReVanced/revanced-manager/issues/new?assignees=&labels=Bug+report&projects=&template=bug_report.yml&title=bug%3A+).
## 🌐 Submitting translations ## 📝 How to contribute
You can contribute translations at translate.revanced.app 1. Before contributing, it is recommended to open an issue to discuss your change
with the maintainers of ReVanced Manager. This will help you determine whether your change is acceptable
and whether it is worth your time to implement it
2. Development happens on the `dev` branch. Fork the repository and create your branch from `dev`
3. Commit your changes
4. Submit a pull request to the `dev` branch of the repository and reference issues
that your pull request closes in the description of your pull request
5. Our team will review your pull request and provide feedback. Once your pull request is approved,
it will be merged into the `dev` branch and will be included in the next release of ReVanced Manager
> [!TIP] ## 🤚 I want to contribute but don't know how to code
> * 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 Even if you don't know how to code, you can still contribute by
translating ReVanced Manager on [Crowdin](https://translate.revanced.app/).
See the guidelines for developing for ReVanced Manager [here](/docs/developer/README.md) ❤️ Thank you for considering contributing to ReVanced Manager,
## 🔒 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 ReVanced

View File

@ -60,8 +60,8 @@
# 💊 ReVanced Manager # 💊 ReVanced Manager
[![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) ![GitHub Workflow Status (with event)](https://img.shields.io/github/actions/workflow/status/ReVanced/revanced-manager/release.yml)
[![GPLv3 License](https://img.shields.io/badge/License-GPL%20v3-yellow.svg)](#-license) ![GPLv3 License](https://img.shields.io/badge/License-GPL%20v3-yellow.svg)
Application to use ReVanced on Android Application to use ReVanced on Android
@ -73,16 +73,13 @@ ReVanced Manager is an application that uses [ReVanced Patcher](https://github.c
Some of the features ReVanced Manager provides are: Some of the features ReVanced Manager provides are:
- 💉 **Patch apps**: Apply any patch of your choice to Android apps - ⬇️ **Download**: Automatically download apps using the ReVanced Manager downloader plugin system
- 📱 **Portable**: ReVanced Patcher that fits in your pocket - 💉 **Patch**: Select and apply patches to any Android app
- 🤗 **Simple UI**: Quickly understand the ins and outs of ReVanced Manager - 🛠️ **Customize**: Manage patches, apps, signing, themes, updates, and many more settings
- 🛠️ **Customization**: Configurable API, custom sources, language, signing keystore, theme and more
## 🔽 Download ## 🔽 Download
You can get the most recent version of ReVanced Manager by downloading from You can download the most recent version of ReVanced Manager at [revanced.app/download](https://revanced.app/download) or from [GitHub releases](https://github.com/ReVanced/revanced-manager/releases/latest).
the [ReVanced site](https://revanced.app/download).
Learn how to use ReVanced Manager by following the [documentation](/docs). Learn how to use ReVanced Manager by following the [documentation](/docs).
## 📚 Everything else ## 📚 Everything else
@ -90,8 +87,11 @@ Learn how to use ReVanced Manager by following the [documentation](/docs).
### 📙 Contributing ### 📙 Contributing
Thank you for considering contributing to ReVanced Manager. Thank you for considering contributing to ReVanced Manager.
You can find the contribution guidelines [here](CONTRIBUTING.md).
The [contribution guidelines](CONTRIBUTING.md) provides information you'll need to open an issue, develop for ReVanced Manager and translations. ### 🛠️ Building
To build a ReVanced Manager, you can follow the [documentation](/docs).
### 📄 Documentation ### 📄 Documentation

View File

@ -1,83 +0,0 @@
<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>
# 🔒 Security Policy
This document describes how to report security vulnerabilities for ReVanced Manager.
## 🚨 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).
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 |
|------------------------------------------|---------------|--------------------|
| ![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

@ -13,7 +13,7 @@ plugins {
android { android {
namespace = "app.revanced.manager" namespace = "app.revanced.manager"
compileSdk = 35 compileSdk = 35
buildToolsVersion = "35.0.0" buildToolsVersion = "35.0.1"
defaultConfig { defaultConfig {
applicationId = "app.revanced.manager" applicationId = "app.revanced.manager"
@ -27,7 +27,8 @@ android {
buildTypes { buildTypes {
debug { debug {
applicationIdSuffix = ".debug" applicationIdSuffix = ".debug"
resValue("string", "app_name", "ReVanced Manager (dev)") resValue("string", "app_name", "ReVanced Manager (Debug)")
isPseudoLocalesEnabled = true
buildConfigField("long", "BUILD_ID", "${Random.nextLong()}L") buildConfigField("long", "BUILD_ID", "${Random.nextLong()}L")
} }
@ -39,10 +40,21 @@ android {
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
} }
if (project.hasProperty("signAsDebug")) { val keystoreFile = file("keystore.jks")
applicationIdSuffix = ".debug"
resValue("string", "app_name", "ReVanced Manager Debug") if (project.hasProperty("signAsDebug") || !keystoreFile.exists()) {
applicationIdSuffix = ".debug_signed"
resValue("string", "app_name", "ReVanced Manager (Debug signed)")
signingConfig = signingConfigs.getByName("debug") signingConfig = signingConfigs.getByName("debug")
isPseudoLocalesEnabled = true
} else {
signingConfig = signingConfigs.create("release") {
storeFile = keystoreFile
storePassword = System.getenv("KEYSTORE_PASSWORD")
keyAlias = System.getenv("KEYSTORE_ENTRY_ALIAS")
keyPassword = System.getenv("KEYSTORE_ENTRY_PASSWORD")
}
} }
buildConfigField("long", "BUILD_ID", "0L") buildConfigField("long", "BUILD_ID", "0L")
@ -60,15 +72,17 @@ android {
} }
packaging { packaging {
resources.excludes.addAll(listOf( resources.excludes.addAll(
"/prebuilt/**", listOf(
"META-INF/DEPENDENCIES", "/prebuilt/**",
"META-INF/**.version", "META-INF/DEPENDENCIES",
"DebugProbesKt.bin", "META-INF/**.version",
"kotlin-tooling-metadata.json", "DebugProbesKt.bin",
"org/bouncycastle/pqc/**.properties", "kotlin-tooling-metadata.json",
"org/bouncycastle/x509/**.properties", "org/bouncycastle/pqc/**.properties",
)) "org/bouncycastle/x509/**.properties",
)
)
jniLibs { jniLibs {
useLegacyPackaging = true useLegacyPackaging = true
} }
@ -158,7 +172,7 @@ dependencies {
implementation(libs.revanced.library) implementation(libs.revanced.library)
// Downloader plugins // Downloader plugins
implementation(project(":downloader-plugin")) implementation(libs.plugin.api)
// Native processes // Native processes
implementation(libs.kotlin.process) implementation(libs.kotlin.process)

View File

@ -9,6 +9,8 @@ import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@ -23,10 +25,32 @@ import androidx.navigation.compose.composable
import androidx.navigation.compose.navigation import androidx.navigation.compose.navigation
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import androidx.navigation.toRoute import androidx.navigation.toRoute
import app.revanced.manager.ui.model.navigation.* import app.revanced.manager.ui.model.navigation.AppSelector
import app.revanced.manager.ui.screen.* import app.revanced.manager.ui.model.navigation.ComplexParameter
import app.revanced.manager.ui.screen.settings.* import app.revanced.manager.ui.model.navigation.Dashboard
import app.revanced.manager.ui.screen.settings.update.ChangelogsScreen import app.revanced.manager.ui.model.navigation.InstalledApplicationInfo
import app.revanced.manager.ui.model.navigation.Patcher
import app.revanced.manager.ui.model.navigation.SelectedApplicationInfo
import app.revanced.manager.ui.model.navigation.Settings
import app.revanced.manager.ui.model.navigation.Update
import app.revanced.manager.ui.screen.AppSelectorScreen
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.PatchesSelectorScreen
import app.revanced.manager.ui.screen.RequiredOptionsScreen
import app.revanced.manager.ui.screen.SelectedAppInfoScreen
import app.revanced.manager.ui.screen.SettingsScreen
import app.revanced.manager.ui.screen.UpdateScreen
import app.revanced.manager.ui.screen.settings.AboutSettingsScreen
import app.revanced.manager.ui.screen.settings.AdvancedSettingsScreen
import app.revanced.manager.ui.screen.settings.ContributorSettingsScreen
import app.revanced.manager.ui.screen.settings.DeveloperSettingsScreen
import app.revanced.manager.ui.screen.settings.DownloadsSettingsScreen
import app.revanced.manager.ui.screen.settings.GeneralSettingsScreen
import app.revanced.manager.ui.screen.settings.ImportExportSettingsScreen
import app.revanced.manager.ui.screen.settings.LicensesSettingsScreen
import app.revanced.manager.ui.screen.settings.update.ChangelogsSettingsScreen
import app.revanced.manager.ui.screen.settings.update.UpdatesSettingsScreen import app.revanced.manager.ui.screen.settings.update.UpdatesSettingsScreen
import app.revanced.manager.ui.theme.ReVancedManagerTheme import app.revanced.manager.ui.theme.ReVancedManagerTheme
import app.revanced.manager.ui.theme.Theme import app.revanced.manager.ui.theme.Theme
@ -89,6 +113,10 @@ private fun ReVancedManager(vm: MainViewModel) {
NavHost( NavHost(
navController = navController, navController = navController,
startDestination = Dashboard, startDestination = Dashboard,
enterTransition = { slideInHorizontally(initialOffsetX = { it }) },
exitTransition = { slideOutHorizontally(targetOffsetX = { -it / 3 }) },
popEnterTransition = { slideInHorizontally(initialOffsetX = { -it / 3 }) },
popExitTransition = { slideOutHorizontally(targetOffsetX = { it }) },
) { ) {
composable<Dashboard> { composable<Dashboard> {
DashboardScreen( DashboardScreen(
@ -136,7 +164,7 @@ private fun ReVancedManager(vm: MainViewModel) {
} }
} }
}, },
vm = koinViewModel { parametersOf(it.getComplexArg<Patcher.ViewModelParams>()) } viewModel = koinViewModel { parametersOf(it.getComplexArg<Patcher.ViewModelParams>()) }
) )
} }
@ -249,6 +277,10 @@ private fun ReVancedManager(vm: MainViewModel) {
AdvancedSettingsScreen(onBackClick = navController::popBackStack) AdvancedSettingsScreen(onBackClick = navController::popBackStack)
} }
composable<Settings.Developer> {
DeveloperSettingsScreen(onBackClick = navController::popBackStack)
}
composable<Settings.Updates> { composable<Settings.Updates> {
UpdatesSettingsScreen( UpdatesSettingsScreen(
onBackClick = navController::popBackStack, onBackClick = navController::popBackStack,
@ -273,20 +305,17 @@ private fun ReVancedManager(vm: MainViewModel) {
} }
composable<Settings.Changelogs> { composable<Settings.Changelogs> {
ChangelogsScreen(onBackClick = navController::popBackStack) ChangelogsSettingsScreen(onBackClick = navController::popBackStack)
} }
composable<Settings.Contributors> { composable<Settings.Contributors> {
ContributorScreen(onBackClick = navController::popBackStack) ContributorSettingsScreen(onBackClick = navController::popBackStack)
} }
composable<Settings.Licenses> { composable<Settings.Licenses> {
LicensesScreen(onBackClick = navController::popBackStack) LicensesSettingsScreen(onBackClick = navController::popBackStack)
} }
composable<Settings.DeveloperOptions> {
DeveloperOptionsScreen(onBackClick = navController::popBackStack)
}
} }
} }
} }

View File

@ -3,6 +3,7 @@ package app.revanced.manager.domain.manager
import android.content.Context import android.content.Context
import app.revanced.manager.domain.manager.base.BasePreferencesManager import app.revanced.manager.domain.manager.base.BasePreferencesManager
import app.revanced.manager.ui.theme.Theme import app.revanced.manager.ui.theme.Theme
import app.revanced.manager.util.isDebuggable
class PreferencesManager( class PreferencesManager(
context: Context context: Context
@ -28,4 +29,6 @@ class PreferencesManager(
val suggestedVersionSafeguard = booleanPreference("suggested_version_safeguard", true) val suggestedVersionSafeguard = booleanPreference("suggested_version_safeguard", true)
val acknowledgedDownloaderPlugins = stringSetPreference("acknowledged_downloader_plugins", emptySet()) val acknowledgedDownloaderPlugins = stringSetPreference("acknowledged_downloader_plugins", emptySet())
val showDeveloperSettings = booleanPreference("show_developer_settings", context.isDebuggable)
} }

View File

@ -111,6 +111,7 @@ class ProcessRuntime(private val context: Context) : Runtime(context) {
} }
val patching = CompletableDeferred<Unit>() val patching = CompletableDeferred<Unit>()
val scope = this
launch(Dispatchers.IO) { launch(Dispatchers.IO) {
val binder = awaitBinderConnection() val binder = awaitBinderConnection()
@ -124,7 +125,7 @@ class ProcessRuntime(private val context: Context) : Runtime(context) {
override fun log(level: String, msg: String) = logger.log(enumValueOf(level), msg) override fun log(level: String, msg: String) = logger.log(enumValueOf(level), msg)
override fun patchSucceeded() { override fun patchSucceeded() {
launch { onPatchCompleted() } scope.launch { onPatchCompleted() }
} }
override fun progress(name: String?, state: String?, msg: String?) = override fun progress(name: String?, state: String?, msg: String?) =
@ -179,7 +180,7 @@ class ProcessRuntime(private val context: Context) : Runtime(context) {
} }
/** /**
* An [Exception] occured in the remote process while patching. * An [Exception] occurred in the remote process while patching.
* *
* @param originalStackTrace The stack trace of the original [Exception]. * @param originalStackTrace The stack trace of the original [Exception].
*/ */

View File

@ -1,8 +1,10 @@
package app.revanced.manager.patcher.runtime.process package app.revanced.manager.patcher.runtime.process
import android.annotation.SuppressLint
import android.app.ActivityThread import android.app.ActivityThread
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Looper import android.os.Looper
import app.revanced.manager.BuildConfig import app.revanced.manager.BuildConfig
@ -95,6 +97,10 @@ class PatcherProcess(private val context: Context) : IPatcherProcess.Stub() {
} }
companion object { companion object {
private val longArrayClass = LongArray::class.java
private val emptyLongArray = LongArray(0)
@SuppressLint("PrivateApi")
@JvmStatic @JvmStatic
fun main(args: Array<String>) { fun main(args: Array<String>) {
Looper.prepare() Looper.prepare()
@ -105,6 +111,15 @@ class PatcherProcess(private val context: Context) : IPatcherProcess.Stub() {
val systemContext = ActivityThread.systemMain().systemContext as Context val systemContext = ActivityThread.systemMain().systemContext as Context
val appContext = systemContext.createPackageContext(managerPackageName, 0) val appContext = systemContext.createPackageContext(managerPackageName, 0)
// Avoid annoying logs. See https://github.com/robolectric/robolectric/blob/ad0484c6b32c7d11176c711abeb3cb4a900f9258/robolectric/src/main/java/org/robolectric/android/internal/AndroidTestEnvironment.java#L376-L388
Class.forName("android.app.AppCompatCallbacks").apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
getDeclaredMethod("install", longArrayClass, longArrayClass).also { it.isAccessible = true }(null, emptyLongArray, emptyLongArray)
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
getDeclaredMethod("install", longArrayClass).also { it.isAccessible = true }(null, emptyLongArray)
}
}
val ipcInterface = PatcherProcess(appContext) val ipcInterface = PatcherProcess(appContext)
appContext.sendBroadcast(Intent().apply { appContext.sendBroadcast(Intent().apply {

View File

@ -4,9 +4,20 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.RowScope
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.* import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -44,9 +55,14 @@ fun AppTopBar(
) )
}, },
actions: @Composable (RowScope.() -> Unit) = {}, actions: @Composable (RowScope.() -> Unit) = {},
scrollBehavior: TopAppBarScrollBehavior? = null scrollBehavior: TopAppBarScrollBehavior? = null,
applyContainerColor: Boolean = false
) { ) {
val containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp) val containerColor = if (applyContainerColor) {
MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp)
} else {
Color.Unspecified
}
TopAppBar( TopAppBar(
title = { Text(title) }, title = { Text(title) },
@ -65,3 +81,41 @@ fun AppTopBar(
) )
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AppTopBar(
title: @Composable () -> Unit,
onBackClick: (() -> Unit)? = null,
backIcon: @Composable (() -> Unit) = @Composable {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(
R.string.back
)
)
},
actions: @Composable (RowScope.() -> Unit) = {},
scrollBehavior: TopAppBarScrollBehavior? = null,
applyContainerColor: Boolean = false
) {
val containerColor = if (applyContainerColor) {
MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp)
} else {
Color.Unspecified
}
TopAppBar(
title = title,
scrollBehavior = scrollBehavior,
navigationIcon = {
if (onBackClick != null) {
IconButton(onClick = onBackClick) {
backIcon()
}
}
},
actions = actions,
colors = TopAppBarDefaults.topAppBarColors(
containerColor = containerColor
)
)
}

View File

@ -0,0 +1,41 @@
package app.revanced.manager.ui.component
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import app.revanced.manager.R
@Composable
fun ConfirmDialog(
onDismiss: () -> Unit,
onConfirm: () -> Unit,
title: String,
description: String,
icon: ImageVector
) {
AlertDialog(
onDismissRequest = onDismiss,
dismissButton = {
TextButton(onDismiss) {
Text(stringResource(R.string.cancel))
}
},
confirmButton = {
TextButton(
onClick = {
onConfirm()
onDismiss()
}
) {
Text(stringResource(R.string.confirm))
}
},
title = { Text(title) },
icon = { Icon(icon, null) },
text = { Text(description) }
)
}

View File

@ -16,8 +16,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.ui.component.bundle.BundleTopBar import app.revanced.manager.ui.component.bundle.BundleTopBar
@ -26,12 +24,8 @@ import app.revanced.manager.ui.component.bundle.BundleTopBar
fun ExceptionViewerDialog(text: String, onDismiss: () -> Unit) { fun ExceptionViewerDialog(text: String, onDismiss: () -> Unit) {
val context = LocalContext.current val context = LocalContext.current
Dialog( FullscreenDialog(
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
properties = DialogProperties(
usePlatformDefaultWidth = false,
dismissOnBackPress = true
)
) { ) {
Scaffold( Scaffold(
topBar = { topBar = {

View File

@ -0,0 +1,34 @@
package app.revanced.manager.ui.component
import android.view.WindowManager
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.compose.ui.window.DialogWindowProvider
private val properties = DialogProperties(
usePlatformDefaultWidth = false,
dismissOnBackPress = true,
decorFitsSystemWindows = false,
)
@Composable
fun FullscreenDialog(onDismissRequest: () -> Unit, content: @Composable () -> Unit) {
Dialog(
onDismissRequest = onDismissRequest,
properties = properties
) {
val window = (LocalView.current.parent as DialogWindowProvider).window
LaunchedEffect(Unit) {
window.statusBarColor = Color.Transparent.toArgb()
window.navigationBarColor = Color.Transparent.toArgb()
window.addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS)
}
content()
}
}

View File

@ -12,17 +12,18 @@ import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.data.platform.NetworkInfo
import app.revanced.manager.domain.bundles.LocalPatchBundle import app.revanced.manager.domain.bundles.LocalPatchBundle
import app.revanced.manager.domain.bundles.PatchBundleSource import app.revanced.manager.domain.bundles.PatchBundleSource
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.asRemoteOrNull import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.asRemoteOrNull
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.isDefault import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.isDefault
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.nameState import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.nameState
import app.revanced.manager.ui.component.ExceptionViewerDialog import app.revanced.manager.ui.component.ExceptionViewerDialog
import app.revanced.manager.ui.component.FullscreenDialog
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.compose.koinInject
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@ -32,6 +33,8 @@ fun BundleInformationDialog(
bundle: PatchBundleSource, bundle: PatchBundleSource,
onUpdate: () -> Unit, onUpdate: () -> Unit,
) { ) {
val networkInfo = koinInject<NetworkInfo>()
val hasNetwork = remember { networkInfo.isConnected() }
val composableScope = rememberCoroutineScope() val composableScope = rememberCoroutineScope()
var viewCurrentBundlePatches by remember { mutableStateOf(false) } var viewCurrentBundlePatches by remember { mutableStateOf(false) }
val isLocal = bundle is LocalPatchBundle val isLocal = bundle is LocalPatchBundle
@ -52,12 +55,8 @@ fun BundleInformationDialog(
) )
} }
Dialog( FullscreenDialog(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
properties = DialogProperties(
usePlatformDefaultWidth = false,
dismissOnBackPress = true
)
) { ) {
val bundleName by bundle.nameState val bundleName by bundle.nameState
@ -81,7 +80,7 @@ fun BundleInformationDialog(
) )
} }
} }
if (!isLocal) { if (!isLocal && hasNetwork) {
IconButton(onClick = onUpdate) { IconButton(onClick = onUpdate) {
Icon( Icon(
Icons.Outlined.Update, Icons.Outlined.Update,

View File

@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material.icons.outlined.ErrorOutline import androidx.compose.material.icons.outlined.ErrorOutline
import androidx.compose.material.icons.outlined.Warning import androidx.compose.material.icons.outlined.Warning
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@ -26,8 +27,9 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.domain.bundles.PatchBundleSource import app.revanced.manager.domain.bundles.PatchBundleSource
import app.revanced.manager.ui.component.haptics.HapticCheckbox
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.nameState import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.nameState
import app.revanced.manager.ui.component.ConfirmDialog
import app.revanced.manager.ui.component.haptics.HapticCheckbox
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@ -42,6 +44,7 @@ fun BundleItem(
toggleSelection: (Boolean) -> Unit, toggleSelection: (Boolean) -> Unit,
) { ) {
var viewBundleDialogPage by rememberSaveable { mutableStateOf(false) } var viewBundleDialogPage by rememberSaveable { mutableStateOf(false) }
var showDeleteConfirmationDialog by rememberSaveable { mutableStateOf(false) }
val state by bundle.state.collectAsStateWithLifecycle() val state by bundle.state.collectAsStateWithLifecycle()
val version by remember(bundle) { val version by remember(bundle) {
@ -52,15 +55,25 @@ fun BundleItem(
if (viewBundleDialogPage) { if (viewBundleDialogPage) {
BundleInformationDialog( BundleInformationDialog(
onDismissRequest = { viewBundleDialogPage = false }, onDismissRequest = { viewBundleDialogPage = false },
onDeleteRequest = { onDeleteRequest = { showDeleteConfirmationDialog = true },
viewBundleDialogPage = false
onDelete()
},
bundle = bundle, bundle = bundle,
onUpdate = onUpdate, onUpdate = onUpdate,
) )
} }
if (showDeleteConfirmationDialog) {
ConfirmDialog(
onDismiss = { showDeleteConfirmationDialog = false },
onConfirm = {
onDelete()
viewBundleDialogPage = false
},
title = stringResource(R.string.bundle_delete_single_dialog_title),
description = stringResource(R.string.bundle_delete_single_dialog_description, name),
icon = Icons.Outlined.Delete
)
}
ListItem( ListItem(
modifier = Modifier modifier = Modifier
.height(64.dp) .height(64.dp)

View File

@ -22,13 +22,12 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.domain.bundles.PatchBundleSource import app.revanced.manager.domain.bundles.PatchBundleSource
import app.revanced.manager.patcher.patch.PatchInfo import app.revanced.manager.patcher.patch.PatchInfo
import app.revanced.manager.ui.component.ArrowButton import app.revanced.manager.ui.component.ArrowButton
import app.revanced.manager.ui.component.FullscreenDialog
import app.revanced.manager.ui.component.LazyColumnWithScrollbar import app.revanced.manager.ui.component.LazyColumnWithScrollbar
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@ -41,12 +40,8 @@ fun BundlePatchesDialog(
var showOptions by rememberSaveable { mutableStateOf(false) } var showOptions by rememberSaveable { mutableStateOf(false) }
val state by bundle.state.collectAsStateWithLifecycle() val state by bundle.state.collectAsStateWithLifecycle()
Dialog( FullscreenDialog(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
properties = DialogProperties(
usePlatformDefaultWidth = false,
dismissOnBackPress = true
)
) { ) {
Scaffold( Scaffold(
topBar = { topBar = {

View File

@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListItemInfo
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
@ -21,8 +22,25 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.DragHandle import androidx.compose.material.icons.filled.DragHandle
import androidx.compose.material.icons.outlined.* import androidx.compose.material.icons.outlined.Add
import androidx.compose.material3.* import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material.icons.outlined.Edit
import androidx.compose.material.icons.outlined.Folder
import androidx.compose.material.icons.outlined.MoreVert
import androidx.compose.material.icons.outlined.Restore
import androidx.compose.material.icons.outlined.SelectAll
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisallowComposableCalls import androidx.compose.runtime.DisallowComposableCalls
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
@ -38,11 +56,15 @@ import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.data.platform.Filesystem import app.revanced.manager.data.platform.Filesystem
import app.revanced.manager.patcher.patch.Option import app.revanced.manager.patcher.patch.Option
import app.revanced.manager.ui.component.* import app.revanced.manager.ui.component.AlertDialogExtended
import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.FloatInputDialog
import app.revanced.manager.ui.component.FullscreenDialog
import app.revanced.manager.ui.component.IntInputDialog
import app.revanced.manager.ui.component.LongInputDialog
import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
import app.revanced.manager.ui.component.haptics.HapticRadioButton import app.revanced.manager.ui.component.haptics.HapticRadioButton
import app.revanced.manager.ui.component.haptics.HapticSwitch import app.revanced.manager.ui.component.haptics.HapticSwitch
@ -52,16 +74,17 @@ import app.revanced.manager.util.saver.snapshotStateListSaver
import app.revanced.manager.util.saver.snapshotStateSetSaver import app.revanced.manager.util.saver.snapshotStateSetSaver
import app.revanced.manager.util.toast import app.revanced.manager.util.toast
import app.revanced.manager.util.transparentListItemColors import app.revanced.manager.util.transparentListItemColors
import kotlinx.coroutines.CoroutineScope
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import org.koin.compose.koinInject import org.koin.compose.koinInject
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.get import org.koin.core.component.get
import sh.calvin.reorderable.ReorderableItem import sh.calvin.reorderable.ReorderableItem
import sh.calvin.reorderable.rememberReorderableLazyColumnState import sh.calvin.reorderable.rememberReorderableLazyColumnState
import sh.calvin.reorderable.rememberReorderableLazyListState
import java.io.Serializable import java.io.Serializable
import kotlin.random.Random import kotlin.random.Random
import kotlin.reflect.typeOf import kotlin.reflect.typeOf
import androidx.compose.ui.window.Dialog as ComposeDialog
private class OptionEditorScope<T : Any>( private class OptionEditorScope<T : Any>(
private val editor: OptionEditor<T>, private val editor: OptionEditor<T>,
@ -476,7 +499,8 @@ private class ListOptionEditor<T : Serializable>(private val elementEditor: Opti
val lazyListState = rememberLazyListState() val lazyListState = rememberLazyListState()
val reorderableLazyColumnState = val reorderableLazyColumnState =
rememberReorderableLazyColumnState(lazyListState) { from, to -> // Update the list
rememberReorderableLazyListState(lazyListState) { from, to ->
// Update the list // Update the list
items.add(to.index, items.removeAt(from.index)) items.add(to.index, items.removeAt(from.index))
} }
@ -503,12 +527,8 @@ private class ListOptionEditor<T : Serializable>(private val elementEditor: Opti
scope.submitDialog(items.mapNotNull { it.value }) scope.submitDialog(items.mapNotNull { it.value })
} }
ComposeDialog( FullscreenDialog(
onDismissRequest = back, onDismissRequest = back,
properties = DialogProperties(
usePlatformDefaultWidth = false,
dismissOnBackPress = true
),
) { ) {
Scaffold( Scaffold(
topBar = { topBar = {
@ -602,8 +622,10 @@ private class ListOptionEditor<T : Serializable>(private val elementEditor: Opti
interactionSource = interactionSource, interactionSource = interactionSource,
onLongClickLabel = stringResource(R.string.select), onLongClickLabel = stringResource(R.string.select),
onLongClick = { onLongClick = {
deletionTargets.add(item.key) if (!deleteMode) {
deleteMode = true deletionTargets.add(item.key)
deleteMode = true
}
}, },
onClick = { onClick = {
if (!deleteMode) { if (!deleteMode) {

View File

@ -23,10 +23,9 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.ui.component.AppTopBar import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.FullscreenDialog
import app.revanced.manager.ui.component.GroupHeader import app.revanced.manager.ui.component.GroupHeader
import app.revanced.manager.ui.component.LazyColumnWithScrollbar import app.revanced.manager.ui.component.LazyColumnWithScrollbar
import app.revanced.manager.util.saver.PathSaver import app.revanced.manager.util.saver.PathSaver
@ -48,12 +47,8 @@ fun PathSelectorDialog(root: Path, onSelect: (Path?) -> Unit) {
currentDirectory.listDirectoryEntries().filter(Path::isReadable).partition(Path::isDirectory) currentDirectory.listDirectoryEntries().filter(Path::isReadable).partition(Path::isDirectory)
} }
Dialog( FullscreenDialog(
onDismissRequest = { onSelect(null) }, onDismissRequest = { onSelect(null) },
properties = DialogProperties(
usePlatformDefaultWidth = false,
dismissOnBackPress = true
)
) { ) {
Scaffold( Scaffold(
topBar = { topBar = {

View File

@ -7,10 +7,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.CalendarToday import androidx.compose.material.icons.outlined.NewReleases
import androidx.compose.material.icons.outlined.Campaign
import androidx.compose.material.icons.outlined.FileDownload
import androidx.compose.material.icons.outlined.Sell
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
@ -37,28 +34,18 @@ fun Changelog(
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Icon( Icon(
imageVector = Icons.Outlined.Campaign, imageVector = Icons.Outlined.NewReleases,
contentDescription = null, contentDescription = null,
tint = MaterialTheme.colorScheme.primary, tint = MaterialTheme.colorScheme.primary,
modifier = Modifier modifier = Modifier
.size(32.dp) .size(32.dp)
) )
Text( Text(
version.removePrefix("v"), "${version.removePrefix("v")} ($publishDate)",
style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight(800)), style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight(800)),
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,
) )
} }
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier
.fillMaxWidth()
) {
Tag(
Icons.Outlined.CalendarToday,
publishDate
)
}
} }
Markdown( Markdown(
markdown, markdown,

View File

@ -11,35 +11,36 @@ import kotlinx.coroutines.flow.map
*/ */
data class BundleInfo( data class BundleInfo(
val name: String, val name: String,
val version: String?,
val uid: Int, val uid: Int,
val supported: List<PatchInfo>, val compatible: List<PatchInfo>,
val unsupported: List<PatchInfo>, val incompatible: List<PatchInfo>,
val universal: List<PatchInfo> val universal: List<PatchInfo>
) { ) {
val all = sequence { val all = sequence {
yieldAll(supported) yieldAll(compatible)
yieldAll(unsupported) yieldAll(incompatible)
yieldAll(universal) yieldAll(universal)
} }
val patchCount get() = supported.size + unsupported.size + universal.size val patchCount get() = compatible.size + incompatible.size + universal.size
fun patchSequence(allowUnsupported: Boolean) = if (allowUnsupported) { fun patchSequence(allowIncompatible: Boolean) = if (allowIncompatible) {
all all
} else { } else {
sequence { sequence {
yieldAll(supported) yieldAll(compatible)
yieldAll(universal) yieldAll(universal)
} }
} }
companion object Extensions { companion object Extensions {
inline fun Iterable<BundleInfo>.toPatchSelection( inline fun Iterable<BundleInfo>.toPatchSelection(
allowUnsupported: Boolean, allowIncompatible: Boolean,
condition: (Int, PatchInfo) -> Boolean condition: (Int, PatchInfo) -> Boolean
): PatchSelection = this.associate { bundle -> ): PatchSelection = this.associate { bundle ->
val patches = val patches =
bundle.patchSequence(allowUnsupported) bundle.patchSequence(allowIncompatible)
.mapNotNullTo(mutableSetOf()) { patch -> .mapNotNullTo(mutableSetOf()) { patch ->
patch.name.takeIf { patch.name.takeIf {
condition( condition(
@ -60,8 +61,8 @@ data class BundleInfo(
source.state.map { state -> source.state.map { state ->
val bundle = state.patchBundleOrNull() ?: return@map null val bundle = state.patchBundleOrNull() ?: return@map null
val supported = mutableListOf<PatchInfo>() val compatible = mutableListOf<PatchInfo>()
val unsupported = mutableListOf<PatchInfo>() val incompatible = mutableListOf<PatchInfo>()
val universal = mutableListOf<PatchInfo>() val universal = mutableListOf<PatchInfo>()
bundle.patches.filter { it.compatibleWith(packageName) }.forEach { bundle.patches.filter { it.compatibleWith(packageName) }.forEach {
@ -70,15 +71,15 @@ data class BundleInfo(
it.supports( it.supports(
packageName, packageName,
version version
) -> supported ) -> compatible
else -> unsupported else -> incompatible
} }
targetList.add(it) targetList.add(it)
} }
BundleInfo(source.getName(), source.uid, supported, unsupported, universal) BundleInfo(source.getName(), source.currentVersion(), source.uid, compatible, incompatible, universal)
} }
} }

View File

@ -92,5 +92,5 @@ object Settings {
data object Licenses : Destination data object Licenses : Destination
@Serializable @Serializable
data object DeveloperOptions : Destination data object Developer : Destination
} }

View File

@ -3,12 +3,26 @@ package app.revanced.manager.ui.screen
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Storage import androidx.compose.material.icons.filled.Storage
import androidx.compose.material.icons.outlined.Search import androidx.compose.material.icons.outlined.Search
import androidx.compose.material3.* import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@ -17,6 +31,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -138,10 +153,13 @@ fun AppSelectorScreen(
} }
} }
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
Scaffold( Scaffold(
topBar = { topBar = {
AppTopBar( AppTopBar(
title = stringResource(R.string.select_app), title = stringResource(R.string.select_app),
scrollBehavior = scrollBehavior,
onBackClick = onBackClick, onBackClick = onBackClick,
actions = { actions = {
IconButton(onClick = { search = true }) { IconButton(onClick = { search = true }) {
@ -149,7 +167,8 @@ fun AppSelectorScreen(
} }
} }
) )
} },
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
) { paddingValues -> ) { paddingValues ->
LazyColumnWithScrollbar( LazyColumnWithScrollbar(
modifier = Modifier modifier = Modifier

View File

@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -19,13 +20,19 @@ fun BundleListScreen(
selectedSources: SnapshotStateList<PatchBundleSource>, selectedSources: SnapshotStateList<PatchBundleSource>,
bundlesSelectable: Boolean, bundlesSelectable: Boolean,
) { ) {
val sortedSources = remember(sources) {
sources.sortedByDescending { source ->
source.state.value.patchBundleOrNull()?.patches?.size ?: 0
}
}
LazyColumnWithScrollbar( LazyColumnWithScrollbar(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top, verticalArrangement = Arrangement.Top,
) { ) {
items( items(
sources, sortedSources,
key = { it.uid } key = { it.uid }
) { source -> ) { source ->
BundleItem( BundleItem(

View File

@ -6,18 +6,49 @@ import android.net.Uri
import android.provider.Settings import android.provider.Settings
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.clickable 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.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.BatteryAlert import androidx.compose.material.icons.filled.BatteryAlert
import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.outlined.* import androidx.compose.material.icons.outlined.Apps
import androidx.compose.material3.* import androidx.compose.material.icons.outlined.BugReport
import androidx.compose.runtime.* import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material.icons.outlined.DeleteOutline
import androidx.compose.material.icons.outlined.Download
import androidx.compose.material.icons.outlined.Refresh
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material.icons.outlined.Source
import androidx.compose.material.icons.outlined.Update
import androidx.compose.material.icons.outlined.WarningAmber
import androidx.compose.material3.Badge
import androidx.compose.material3.BadgedBox
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.TabRow
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
@ -32,10 +63,11 @@ import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.AutoUpdatesDialog import app.revanced.manager.ui.component.AutoUpdatesDialog
import app.revanced.manager.ui.component.AvailableUpdateDialog import app.revanced.manager.ui.component.AvailableUpdateDialog
import app.revanced.manager.ui.component.NotificationCard import app.revanced.manager.ui.component.NotificationCard
import app.revanced.manager.ui.component.ConfirmDialog
import app.revanced.manager.ui.component.bundle.BundleTopBar import app.revanced.manager.ui.component.bundle.BundleTopBar
import app.revanced.manager.ui.component.bundle.ImportPatchBundleDialog
import app.revanced.manager.ui.component.haptics.HapticFloatingActionButton import app.revanced.manager.ui.component.haptics.HapticFloatingActionButton
import app.revanced.manager.ui.component.haptics.HapticTab import app.revanced.manager.ui.component.haptics.HapticTab
import app.revanced.manager.ui.component.bundle.ImportPatchBundleDialog
import app.revanced.manager.ui.viewmodel.DashboardViewModel import app.revanced.manager.ui.viewmodel.DashboardViewModel
import app.revanced.manager.util.RequestInstallAppsContract import app.revanced.manager.util.RequestInstallAppsContract
import app.revanced.manager.util.toast import app.revanced.manager.util.toast
@ -124,6 +156,20 @@ fun DashboardScreen(
} }
) )
var showDeleteConfirmationDialog by rememberSaveable { mutableStateOf(false) }
if (showDeleteConfirmationDialog) {
ConfirmDialog(
onDismiss = { showDeleteConfirmationDialog = false },
onConfirm = {
vm.selectedSources.forEach { if (!it.isDefault) vm.delete(it) }
vm.cancelSourceSelection()
},
title = stringResource(R.string.bundle_delete_multiple_dialog_title),
description = stringResource(R.string.bundle_delete_multiple_dialog_description),
icon = Icons.Outlined.Delete
)
}
Scaffold( Scaffold(
topBar = { topBar = {
if (bundlesSelectable) { if (bundlesSelectable) {
@ -139,8 +185,7 @@ fun DashboardScreen(
actions = { actions = {
IconButton( IconButton(
onClick = { onClick = {
vm.selectedSources.forEach { if (!it.isDefault) vm.delete(it) } showDeleteConfirmationDialog = true
vm.cancelSourceSelection()
} }
) { ) {
Icon( Icon(
@ -171,11 +216,7 @@ fun DashboardScreen(
) { ) {
BadgedBox( BadgedBox(
badge = { badge = {
Badge( Badge(modifier = Modifier.size(6.dp))
// A size value above 6.dp forces the Badge icon to be closer to the center, fixing a clipping issue
modifier = Modifier.size(7.dp),
containerColor = MaterialTheme.colorScheme.primary,
)
} }
) { ) {
Icon(Icons.Outlined.Update, stringResource(R.string.update)) Icon(Icons.Outlined.Update, stringResource(R.string.update))
@ -185,7 +226,8 @@ fun DashboardScreen(
IconButton(onClick = onSettingsClick) { IconButton(onClick = onSettingsClick) {
Icon(Icons.Outlined.Settings, stringResource(R.string.settings)) Icon(Icons.Outlined.Settings, stringResource(R.string.settings))
} }
} },
applyContainerColor = true
) )
} }
}, },
@ -238,7 +280,6 @@ fun DashboardScreen(
} }
} }
val showBatteryOptimizationsWarning by vm.showBatteryOptimizationsWarningFlow.collectAsStateWithLifecycle(false)
Notifications( Notifications(
if (!Aapt.supportsDevice()) { if (!Aapt.supportsDevice()) {
{ {
@ -250,16 +291,23 @@ fun DashboardScreen(
) )
} }
} else null, } else null,
if (showBatteryOptimizationsWarning) { if (vm.showBatteryOptimizationsWarning) {
{ {
val batteryOptimizationsLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
vm.updateBatteryOptimizationsWarning()
}
NotificationCard( NotificationCard(
isWarning = true, isWarning = true,
icon = Icons.Default.BatteryAlert, icon = Icons.Default.BatteryAlert,
text = stringResource(R.string.battery_optimization_notification), text = stringResource(R.string.battery_optimization_notification),
onClick = { onClick = {
androidContext.startActivity(Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply { batteryOptimizationsLauncher.launch(
data = Uri.parse("package:${androidContext.packageName}") Intent(
}) Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,
Uri.fromParts("package", androidContext.packageName, null)
)
)
} }
) )
} }

View File

@ -21,6 +21,8 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@ -29,6 +31,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@ -64,13 +67,17 @@ fun InstalledAppInfoScreen(
onConfirm = { viewModel.uninstall() } onConfirm = { viewModel.uninstall() }
) )
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
Scaffold( Scaffold(
topBar = { topBar = {
AppTopBar( AppTopBar(
title = stringResource(R.string.app_info), title = stringResource(R.string.app_info),
scrollBehavior = scrollBehavior,
onBackClick = onBackClick onBackClick = onBackClick
) )
} },
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
) { paddingValues -> ) { paddingValues ->
ColumnWithScrollbar( ColumnWithScrollbar(
modifier = Modifier modifier = Modifier

View File

@ -7,15 +7,28 @@ import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.result.contract.ActivityResultContracts.CreateDocument import androidx.activity.result.contract.ActivityResultContracts.CreateDocument
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.OpenInNew import androidx.compose.material.icons.automirrored.outlined.OpenInNew
import androidx.compose.material.icons.outlined.Cancel
import androidx.compose.material.icons.outlined.FileDownload import androidx.compose.material.icons.outlined.FileDownload
import androidx.compose.material.icons.outlined.PostAdd import androidx.compose.material.icons.outlined.PostAdd
import androidx.compose.material.icons.outlined.Save import androidx.compose.material.icons.outlined.Save
import androidx.compose.material3.* import androidx.compose.material3.AlertDialog
import androidx.compose.material3.BottomAppBar
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
@ -33,6 +46,7 @@ import app.revanced.manager.R
import app.revanced.manager.data.room.apps.installed.InstallType import app.revanced.manager.data.room.apps.installed.InstallType
import app.revanced.manager.ui.component.AppScaffold import app.revanced.manager.ui.component.AppScaffold
import app.revanced.manager.ui.component.AppTopBar import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.ConfirmDialog
import app.revanced.manager.ui.component.InstallerStatusDialog import app.revanced.manager.ui.component.InstallerStatusDialog
import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
import app.revanced.manager.ui.component.patcher.InstallPickerDialog import app.revanced.manager.ui.component.patcher.InstallPickerDialog
@ -46,25 +60,23 @@ import app.revanced.manager.util.EventEffect
@Composable @Composable
fun PatcherScreen( fun PatcherScreen(
onBackClick: () -> Unit, onBackClick: () -> Unit,
vm: PatcherViewModel viewModel: PatcherViewModel
) { ) {
fun leaveScreen() {
vm.onBack()
onBackClick()
}
BackHandler(onBack = ::leaveScreen)
val context = LocalContext.current val context = LocalContext.current
val exportApkLauncher = val exportApkLauncher =
rememberLauncherForActivityResult(CreateDocument(APK_MIMETYPE), vm::export) rememberLauncherForActivityResult(CreateDocument(APK_MIMETYPE), viewModel::export)
val patcherSucceeded by vm.patcherSucceeded.observeAsState(null) val patcherSucceeded by viewModel.patcherSucceeded.observeAsState(null)
val canInstall by remember { derivedStateOf { patcherSucceeded == true && (vm.installedPackageName != null || !vm.isInstalling) } } val canInstall by remember { derivedStateOf { patcherSucceeded == true && (viewModel.installedPackageName != null || !viewModel.isInstalling) } }
var showInstallPicker by rememberSaveable { mutableStateOf(false) } var showInstallPicker by rememberSaveable { mutableStateOf(false) }
var showDismissConfirmationDialog by rememberSaveable { mutableStateOf(false) }
BackHandler(onBack = { showDismissConfirmationDialog = true })
val steps by remember { val steps by remember {
derivedStateOf { derivedStateOf {
vm.steps.groupBy { it.category } viewModel.steps.groupBy { it.category }
} }
} }
@ -81,34 +93,47 @@ fun PatcherScreen(
if (showInstallPicker) if (showInstallPicker)
InstallPickerDialog( InstallPickerDialog(
onDismiss = { showInstallPicker = false }, onDismiss = { showInstallPicker = false },
onConfirm = vm::install onConfirm = viewModel::install
) )
vm.packageInstallerStatus?.let { if (showDismissConfirmationDialog) {
InstallerStatusDialog(it, vm, vm::dismissPackageInstallerDialog) ConfirmDialog(
onDismiss = { showDismissConfirmationDialog = false },
onConfirm = {
viewModel.onBack()
onBackClick()
},
title = stringResource(R.string.patcher_stop_confirm_title),
description = stringResource(R.string.patcher_stop_confirm_description),
icon = Icons.Outlined.Cancel
)
}
viewModel.packageInstallerStatus?.let {
InstallerStatusDialog(it, viewModel, viewModel::dismissPackageInstallerDialog)
} }
val activityLauncher = rememberLauncherForActivityResult( val activityLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult(), contract = ActivityResultContracts.StartActivityForResult(),
onResult = vm::handleActivityResult onResult = viewModel::handleActivityResult
) )
EventEffect(flow = vm.launchActivityFlow) { intent -> EventEffect(flow = viewModel.launchActivityFlow) { intent ->
activityLauncher.launch(intent) activityLauncher.launch(intent)
} }
vm.activityPromptDialog?.let { title -> viewModel.activityPromptDialog?.let { title ->
AlertDialog( AlertDialog(
onDismissRequest = vm::rejectInteraction, onDismissRequest = viewModel::rejectInteraction,
confirmButton = { confirmButton = {
TextButton( TextButton(
onClick = vm::allowInteraction onClick = viewModel::allowInteraction
) { ) {
Text(stringResource(R.string.continue_)) Text(stringResource(R.string.continue_))
} }
}, },
dismissButton = { dismissButton = {
TextButton( TextButton(
onClick = vm::rejectInteraction onClick = viewModel::rejectInteraction
) { ) {
Text(stringResource(R.string.cancel)) Text(stringResource(R.string.cancel))
} }
@ -121,23 +146,24 @@ fun PatcherScreen(
} }
AppScaffold( AppScaffold(
topBar = { topBar = { scrollBehavior ->
AppTopBar( AppTopBar(
title = stringResource(R.string.patcher), title = stringResource(R.string.patcher),
onBackClick = ::leaveScreen scrollBehavior = scrollBehavior,
onBackClick = { showDismissConfirmationDialog = true }
) )
}, },
bottomBar = { bottomBar = {
BottomAppBar( BottomAppBar(
actions = { actions = {
IconButton( IconButton(
onClick = { exportApkLauncher.launch("${vm.packageName}.apk") }, onClick = { exportApkLauncher.launch("${viewModel.packageName}_${viewModel.version}_revanced_patched.apk") },
enabled = patcherSucceeded == true enabled = patcherSucceeded == true
) { ) {
Icon(Icons.Outlined.Save, stringResource(id = R.string.save_apk)) Icon(Icons.Outlined.Save, stringResource(id = R.string.save_apk))
} }
IconButton( IconButton(
onClick = { vm.exportLogs(context) }, onClick = { viewModel.exportLogs(context) },
enabled = patcherSucceeded != null enabled = patcherSucceeded != null
) { ) {
Icon(Icons.Outlined.PostAdd, stringResource(id = R.string.save_logs)) Icon(Icons.Outlined.PostAdd, stringResource(id = R.string.save_logs))
@ -148,11 +174,11 @@ fun PatcherScreen(
HapticExtendedFloatingActionButton( HapticExtendedFloatingActionButton(
text = { text = {
Text( Text(
stringResource(if (vm.installedPackageName == null) R.string.install_app else R.string.open_app) stringResource(if (viewModel.installedPackageName == null) R.string.install_app else R.string.open_app)
) )
}, },
icon = { icon = {
vm.installedPackageName?.let { viewModel.installedPackageName?.let {
Icon( Icon(
Icons.AutoMirrored.Outlined.OpenInNew, Icons.AutoMirrored.Outlined.OpenInNew,
stringResource(R.string.open_app) stringResource(R.string.open_app)
@ -163,10 +189,10 @@ fun PatcherScreen(
) )
}, },
onClick = { onClick = {
if (vm.installedPackageName == null) if (viewModel.installedPackageName == null)
if (vm.isDeviceRooted()) showInstallPicker = true if (viewModel.isDeviceRooted()) showInstallPicker = true
else vm.install(InstallType.DEFAULT) else viewModel.install(InstallType.DEFAULT)
else vm.open() else viewModel.open()
} }
) )
} }
@ -180,7 +206,7 @@ fun PatcherScreen(
.fillMaxSize() .fillMaxSize()
) { ) {
LinearProgressIndicator( LinearProgressIndicator(
progress = { vm.progress }, progress = { viewModel.progress },
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
@ -196,8 +222,8 @@ fun PatcherScreen(
Steps( Steps(
category = category, category = category,
steps = steps, steps = steps,
stepCount = if (category == StepCategory.PATCHING) vm.patchesProgress else null, stepCount = if (category == StepCategory.PATCHING) viewModel.patchesProgress else null,
stepProgressProvider = vm stepProgressProvider = viewModel
) )
} }
} }

View File

@ -60,14 +60,13 @@ import androidx.compose.ui.draw.rotate
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.patcher.patch.Option import app.revanced.manager.patcher.patch.Option
import app.revanced.manager.patcher.patch.PatchInfo import app.revanced.manager.patcher.patch.PatchInfo
import app.revanced.manager.ui.component.AppTopBar import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.CheckedFilterChip import app.revanced.manager.ui.component.CheckedFilterChip
import app.revanced.manager.ui.component.FullscreenDialog
import app.revanced.manager.ui.component.LazyColumnWithScrollbar import app.revanced.manager.ui.component.LazyColumnWithScrollbar
import app.revanced.manager.ui.component.SafeguardDialog import app.revanced.manager.ui.component.SafeguardDialog
import app.revanced.manager.ui.component.SearchBar import app.revanced.manager.ui.component.SearchBar
@ -76,7 +75,7 @@ import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionBut
import app.revanced.manager.ui.component.haptics.HapticTab import app.revanced.manager.ui.component.haptics.HapticTab
import app.revanced.manager.ui.component.patches.OptionItem import app.revanced.manager.ui.component.patches.OptionItem
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_UNSUPPORTED import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_INCOMPATIBLE
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_UNIVERSAL import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_UNIVERSAL
import app.revanced.manager.util.Options import app.revanced.manager.util.Options
import app.revanced.manager.util.PatchSelection import app.revanced.manager.util.PatchSelection
@ -147,9 +146,9 @@ fun PatchesSelectorScreen(
verticalArrangement = Arrangement.spacedBy(12.dp) verticalArrangement = Arrangement.spacedBy(12.dp)
) { ) {
CheckedFilterChip( CheckedFilterChip(
selected = vm.filter and SHOW_UNSUPPORTED == 0, selected = vm.filter and SHOW_INCOMPATIBLE == 0,
onClick = { vm.toggleFlag(SHOW_UNSUPPORTED) }, onClick = { vm.toggleFlag(SHOW_INCOMPATIBLE) },
label = { Text(stringResource(R.string.supported)) } label = { Text(stringResource(R.string.this_version)) }
) )
CheckedFilterChip( CheckedFilterChip(
@ -163,18 +162,18 @@ fun PatchesSelectorScreen(
} }
if (vm.compatibleVersions.isNotEmpty()) if (vm.compatibleVersions.isNotEmpty())
UnsupportedPatchDialog( IncompatiblePatchDialog(
appVersion = vm.appVersion ?: stringResource(R.string.any_version), appVersion = vm.appVersion ?: stringResource(R.string.any_version),
supportedVersions = vm.compatibleVersions, compatibleVersions = vm.compatibleVersions,
onDismissRequest = vm::dismissDialogs onDismissRequest = vm::dismissDialogs
) )
var showUnsupportedPatchesDialog by rememberSaveable { var showIncompatiblePatchesDialog by rememberSaveable {
mutableStateOf(false) mutableStateOf(false)
} }
if (showUnsupportedPatchesDialog) if (showIncompatiblePatchesDialog)
UnsupportedPatchesDialog( IncompatiblePatchesDialog(
appVersion = vm.appVersion ?: stringResource(R.string.any_version), appVersion = vm.appVersion ?: stringResource(R.string.any_version),
onDismissRequest = { showUnsupportedPatchesDialog = false } onDismissRequest = { showIncompatiblePatchesDialog = false }
) )
vm.optionsDialog?.let { (bundle, patch) -> vm.optionsDialog?.let { (bundle, patch) ->
@ -204,7 +203,7 @@ fun PatchesSelectorScreen(
uid: Int, uid: Int,
patches: List<PatchInfo>, patches: List<PatchInfo>,
visible: Boolean, visible: Boolean,
supported: Boolean, compatible: Boolean,
header: (@Composable () -> Unit)? = null header: (@Composable () -> Unit)? = null
) { ) {
if (patches.isNotEmpty() && visible) { if (patches.isNotEmpty() && visible) {
@ -224,14 +223,14 @@ fun PatchesSelectorScreen(
onOptionsDialog = { onOptionsDialog = {
vm.optionsDialog = uid to patch vm.optionsDialog = uid to patch
}, },
selected = supported && vm.isSelected( selected = compatible && vm.isSelected(
uid, uid,
patch patch
), ),
onToggle = { onToggle = {
when { when {
// Open unsupported dialog if the patch is not supported // Open incompatible dialog if the patch is not supported
!supported -> vm.openUnsupportedDialog(patch) !compatible -> vm.openIncompatibleDialog(patch)
// Show selection warning if enabled // Show selection warning if enabled
vm.selectionWarningEnabled -> showSelectionWarning = true vm.selectionWarningEnabled -> showSelectionWarning = true
@ -245,7 +244,7 @@ fun PatchesSelectorScreen(
else -> vm.togglePatch(uid, patch) else -> vm.togglePatch(uid, patch)
} }
}, },
supported = supported compatible = compatible
) )
} }
} }
@ -321,15 +320,15 @@ fun PatchesSelectorScreen(
patchList( patchList(
uid = bundle.uid, uid = bundle.uid,
patches = bundle.supported.searched(), patches = bundle.compatible.searched(),
visible = true, visible = true,
supported = true compatible = true
) )
patchList( patchList(
uid = bundle.uid, uid = bundle.uid,
patches = bundle.universal.searched(), patches = bundle.universal.searched(),
visible = vm.filter and SHOW_UNIVERSAL != 0, visible = vm.filter and SHOW_UNIVERSAL != 0,
supported = true compatible = true
) { ) {
ListHeader( ListHeader(
title = stringResource(R.string.universal_patches), title = stringResource(R.string.universal_patches),
@ -338,13 +337,13 @@ fun PatchesSelectorScreen(
patchList( patchList(
uid = bundle.uid, uid = bundle.uid,
patches = bundle.unsupported.searched(), patches = bundle.incompatible.searched(),
visible = vm.filter and SHOW_UNSUPPORTED != 0, visible = vm.filter and SHOW_INCOMPATIBLE != 0,
supported = vm.allowIncompatiblePatches compatible = vm.allowIncompatiblePatches
) { ) {
ListHeader( ListHeader(
title = stringResource(R.string.unsupported_patches), title = stringResource(R.string.incompatible_patches),
onHelpClick = { showUnsupportedPatchesDialog = true } onHelpClick = { showIncompatiblePatchesDialog = true }
) )
} }
} }
@ -369,14 +368,22 @@ fun PatchesSelectorScreen(
Icon(Icons.Outlined.Restore, stringResource(R.string.reset)) Icon(Icons.Outlined.Restore, stringResource(R.string.reset))
} }
HapticExtendedFloatingActionButton( HapticExtendedFloatingActionButton(
text = { Text(stringResource(R.string.save_with_count, selectedPatchCount)) }, text = {
Text(
stringResource(
R.string.save_with_count,
selectedPatchCount
)
)
},
icon = { icon = {
Icon( Icon(
imageVector = Icons.Outlined.Save, imageVector = Icons.Outlined.Save,
contentDescription = stringResource(R.string.save) contentDescription = stringResource(R.string.save)
) )
}, },
expanded = patchLazyListStates.getOrNull(pagerState.currentPage)?.isScrollingUp ?: true, expanded = patchLazyListStates.getOrNull(pagerState.currentPage)?.isScrollingUp
?: true,
onClick = { onClick = {
onSave(vm.getCustomSelection(), vm.getOptions()) onSave(vm.getCustomSelection(), vm.getOptions())
} }
@ -405,7 +412,19 @@ fun PatchesSelectorScreen(
) )
} }
}, },
text = { Text(bundle.name) }, text = {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
text = bundle.name,
style = MaterialTheme.typography.bodyMedium
)
Text(
text = bundle.version!!,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
},
selectedContentColor = MaterialTheme.colorScheme.primary, selectedContentColor = MaterialTheme.colorScheme.primary,
unselectedContentColor = MaterialTheme.colorScheme.onSurfaceVariant unselectedContentColor = MaterialTheme.colorScheme.onSurfaceVariant
) )
@ -427,15 +446,15 @@ fun PatchesSelectorScreen(
) { ) {
patchList( patchList(
uid = bundle.uid, uid = bundle.uid,
patches = bundle.supported, patches = bundle.compatible,
visible = true, visible = true,
supported = true compatible = true
) )
patchList( patchList(
uid = bundle.uid, uid = bundle.uid,
patches = bundle.universal, patches = bundle.universal,
visible = vm.filter and SHOW_UNIVERSAL != 0, visible = vm.filter and SHOW_UNIVERSAL != 0,
supported = true compatible = true
) { ) {
ListHeader( ListHeader(
title = stringResource(R.string.universal_patches), title = stringResource(R.string.universal_patches),
@ -443,13 +462,13 @@ fun PatchesSelectorScreen(
} }
patchList( patchList(
uid = bundle.uid, uid = bundle.uid,
patches = bundle.unsupported, patches = bundle.incompatible,
visible = vm.filter and SHOW_UNSUPPORTED != 0, visible = vm.filter and SHOW_INCOMPATIBLE != 0,
supported = vm.allowIncompatiblePatches compatible = vm.allowIncompatiblePatches
) { ) {
ListHeader( ListHeader(
title = stringResource(R.string.unsupported_patches), title = stringResource(R.string.incompatible_patches),
onHelpClick = { showUnsupportedPatchesDialog = true } onHelpClick = { showIncompatiblePatchesDialog = true }
) )
} }
} }
@ -506,24 +525,24 @@ private fun PatchItem(
onOptionsDialog: () -> Unit, onOptionsDialog: () -> Unit,
selected: Boolean, selected: Boolean,
onToggle: () -> Unit, onToggle: () -> Unit,
supported: Boolean = true compatible: Boolean = true
) = ListItem( ) = ListItem(
modifier = Modifier modifier = Modifier
.let { if (!supported) it.alpha(0.5f) else it } .let { if (!compatible) it.alpha(0.5f) else it }
.clickable(onClick = onToggle) .clickable(onClick = onToggle)
.fillMaxSize(), .fillMaxSize(),
leadingContent = { leadingContent = {
HapticCheckbox( HapticCheckbox(
checked = selected, checked = selected,
onCheckedChange = { onToggle() }, onCheckedChange = { onToggle() },
enabled = supported enabled = compatible
) )
}, },
headlineContent = { Text(patch.name) }, headlineContent = { Text(patch.name) },
supportingContent = patch.description?.let { { Text(it) } }, supportingContent = patch.description?.let { { Text(it) } },
trailingContent = { trailingContent = {
if (patch.options?.isNotEmpty() == true) { if (patch.options?.isNotEmpty() == true) {
IconButton(onClick = onOptionsDialog, enabled = supported) { IconButton(onClick = onOptionsDialog, enabled = compatible) {
Icon(Icons.Outlined.Settings, null) Icon(Icons.Outlined.Settings, null)
} }
} }
@ -559,7 +578,7 @@ fun ListHeader(
} }
@Composable @Composable
private fun UnsupportedPatchesDialog( private fun IncompatiblePatchesDialog(
appVersion: String, appVersion: String,
onDismissRequest: () -> Unit onDismissRequest: () -> Unit
) = AlertDialog( ) = AlertDialog(
@ -572,11 +591,11 @@ private fun UnsupportedPatchesDialog(
Text(stringResource(R.string.ok)) Text(stringResource(R.string.ok))
} }
}, },
title = { Text(stringResource(R.string.unsupported_patches)) }, title = { Text(stringResource(R.string.incompatible_patches)) },
text = { text = {
Text( Text(
stringResource( stringResource(
R.string.unsupported_patches_dialog, R.string.incompatible_patches_dialog,
appVersion appVersion
) )
) )
@ -584,9 +603,9 @@ private fun UnsupportedPatchesDialog(
) )
@Composable @Composable
private fun UnsupportedPatchDialog( private fun IncompatiblePatchDialog(
appVersion: String, appVersion: String,
supportedVersions: List<String>, compatibleVersions: List<String>,
onDismissRequest: () -> Unit onDismissRequest: () -> Unit
) = AlertDialog( ) = AlertDialog(
icon = { icon = {
@ -598,13 +617,13 @@ private fun UnsupportedPatchDialog(
Text(stringResource(R.string.ok)) Text(stringResource(R.string.ok))
} }
}, },
title = { Text(stringResource(R.string.unsupported_patch)) }, title = { Text(stringResource(R.string.incompatible_patch)) },
text = { text = {
Text( Text(
stringResource( stringResource(
R.string.app_not_supported, R.string.app_version_not_compatible,
appVersion, appVersion,
supportedVersions.joinToString(", ") compatibleVersions.joinToString(", ")
) )
) )
} }
@ -618,13 +637,7 @@ private fun OptionsDialog(
reset: () -> Unit, reset: () -> Unit,
set: (String, Any?) -> Unit, set: (String, Any?) -> Unit,
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
) = Dialog( ) = FullscreenDialog(onDismissRequest = onDismissRequest) {
onDismissRequest = onDismissRequest,
properties = DialogProperties(
usePlatformDefaultWidth = false,
dismissOnBackPress = true
)
) {
Scaffold( Scaffold(
topBar = { topBar = {
AppTopBar( AppTopBar(

View File

@ -15,6 +15,8 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.ScrollableTabRow import androidx.compose.material3.ScrollableTabRow
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
@ -22,6 +24,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
@ -66,10 +69,13 @@ fun RequiredOptionsScreen(
} }
val composableScope = rememberCoroutineScope() val composableScope = rememberCoroutineScope()
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
Scaffold( Scaffold(
topBar = { topBar = {
AppTopBar( AppTopBar(
title = stringResource(R.string.required_options_screen), title = stringResource(R.string.required_options_screen),
scrollBehavior = scrollBehavior,
onBackClick = onBackClick onBackClick = onBackClick
) )
}, },
@ -90,7 +96,8 @@ fun RequiredOptionsScreen(
onContinue(vm.getCustomSelection(), vm.getOptions()) onContinue(vm.getCustomSelection(), vm.getOptions())
} }
) )
} },
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
) { paddingValues -> ) { paddingValues ->
Column( Column(
Modifier Modifier

View File

@ -4,6 +4,8 @@ import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
@ -12,17 +14,28 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.ArrowRight import androidx.compose.material.icons.automirrored.outlined.ArrowRight
import androidx.compose.material.icons.filled.AutoFixHigh import androidx.compose.material.icons.filled.AutoFixHigh
import androidx.compose.material3.* import androidx.compose.material.icons.outlined.WarningAmber
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.data.platform.NetworkInfo
import app.revanced.manager.data.room.apps.installed.InstallType import app.revanced.manager.data.room.apps.installed.InstallType
import app.revanced.manager.data.room.apps.installed.InstalledApp import app.revanced.manager.data.room.apps.installed.InstalledApp
import app.revanced.manager.network.downloader.LoadedDownloaderPlugin import app.revanced.manager.network.downloader.LoadedDownloaderPlugin
@ -31,6 +44,7 @@ import app.revanced.manager.ui.component.AppInfo
import app.revanced.manager.ui.component.AppTopBar import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.ColumnWithScrollbar import app.revanced.manager.ui.component.ColumnWithScrollbar
import app.revanced.manager.ui.component.LoadingIndicator import app.revanced.manager.ui.component.LoadingIndicator
import app.revanced.manager.ui.component.NotificationCard
import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
import app.revanced.manager.ui.model.SelectedApp import app.revanced.manager.ui.model.SelectedApp
import app.revanced.manager.ui.viewmodel.SelectedAppInfoViewModel import app.revanced.manager.ui.viewmodel.SelectedAppInfoViewModel
@ -41,6 +55,7 @@ import app.revanced.manager.util.enabled
import app.revanced.manager.util.toast import app.revanced.manager.util.toast
import app.revanced.manager.util.transparentListItemColors import app.revanced.manager.util.transparentListItemColors
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.compose.koinInject
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@ -52,6 +67,9 @@ fun SelectedAppInfoScreen(
vm: SelectedAppInfoViewModel vm: SelectedAppInfoViewModel
) { ) {
val context = LocalContext.current val context = LocalContext.current
val networkInfo = koinInject<NetworkInfo>()
val networkConnected = remember { networkInfo.isConnected() }
val networkMetered = remember { !networkInfo.isUnmetered() }
val packageName = vm.selectedApp.packageName val packageName = vm.selectedApp.packageName
val version = vm.selectedApp.version val version = vm.selectedApp.version
@ -75,10 +93,14 @@ fun SelectedAppInfoScreen(
val composableScope = rememberCoroutineScope() val composableScope = rememberCoroutineScope()
val error by vm.errorFlow.collectAsStateWithLifecycle(null) val error by vm.errorFlow.collectAsStateWithLifecycle(null)
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
Scaffold( Scaffold(
topBar = { topBar = {
AppTopBar( AppTopBar(
title = stringResource(R.string.app_info), title = stringResource(R.string.app_info),
scrollBehavior = scrollBehavior,
onBackClick = onBackClick onBackClick = onBackClick
) )
}, },
@ -114,7 +136,8 @@ fun SelectedAppInfoScreen(
} }
} }
) )
} },
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
) { paddingValues -> ) { paddingValues ->
val plugins by vm.plugins.collectAsStateWithLifecycle(emptyList()) val plugins by vm.plugins.collectAsStateWithLifecycle(emptyList())
@ -194,6 +217,35 @@ fun SelectedAppInfoScreen(
modifier = Modifier.padding(horizontal = 24.dp) modifier = Modifier.padding(horizontal = 24.dp)
) )
} }
Column(
modifier = Modifier.padding(horizontal = 24.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
val needsInternet =
vm.selectedApp.let { it is SelectedApp.Search || it is SelectedApp.Download }
when {
!needsInternet -> {}
!networkConnected -> {
NotificationCard(
isWarning = true,
icon = Icons.Outlined.WarningAmber,
text = stringResource(R.string.network_unavailable_warning),
onDismiss = null
)
}
networkMetered -> {
NotificationCard(
isWarning = true,
icon = Icons.Outlined.WarningAmber,
text = stringResource(R.string.network_metered_warning),
onDismiss = null
)
}
}
}
} }
} }
} }

View File

@ -1,5 +1,6 @@
package app.revanced.manager.ui.screen package app.revanced.manager.ui.screen
import androidx.annotation.StringRes
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
@ -7,50 +8,79 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.* import androidx.compose.material.icons.outlined.*
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.ui.component.AppTopBar import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.ColumnWithScrollbar import app.revanced.manager.ui.component.ColumnWithScrollbar
import app.revanced.manager.ui.component.settings.SettingsListItem import app.revanced.manager.ui.component.settings.SettingsListItem
import app.revanced.manager.ui.model.navigation.Settings import app.revanced.manager.ui.model.navigation.Settings
import org.koin.compose.koinInject
private val settingsSections = listOf( private data class Section(
Triple( @StringRes val name: Int,
R.string.general, @StringRes val description: Int,
R.string.general_description, val image: ImageVector,
Icons.Outlined.Settings val destination: Settings.Destination,
) 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) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun SettingsScreen(onBackClick: () -> Unit, navigate: (Settings.Destination) -> Unit) { fun SettingsScreen(onBackClick: () -> Unit, navigate: (Settings.Destination) -> Unit) {
val prefs: PreferencesManager = koinInject()
val showDeveloperSettings by prefs.showDeveloperSettings.getAsState()
val settingsSections = remember(showDeveloperSettings) {
listOfNotNull(
Section(
R.string.general,
R.string.general_description,
Icons.Outlined.Settings,
Settings.General
),
Section(
R.string.updates,
R.string.updates_description,
Icons.Outlined.Update,
Settings.Updates
),
Section(
R.string.downloads,
R.string.downloads_description,
Icons.Outlined.Download,
Settings.Downloads
),
Section(
R.string.import_export,
R.string.import_export_description,
Icons.Outlined.SwapVert,
Settings.ImportExport
),
Section(
R.string.advanced,
R.string.advanced_description,
Icons.Outlined.Tune,
Settings.Advanced
),
Section(
R.string.about,
R.string.app_name,
Icons.Outlined.Info,
Settings.About
),
Section(
R.string.developer_options,
R.string.developer_options_description,
Icons.Outlined.Code,
Settings.Developer
).takeIf { showDeveloperSettings }
)
}
Scaffold( Scaffold(
topBar = { topBar = {
AppTopBar( AppTopBar(
@ -64,12 +94,12 @@ fun SettingsScreen(onBackClick: () -> Unit, navigate: (Settings.Destination) ->
.padding(paddingValues) .padding(paddingValues)
.fillMaxSize() .fillMaxSize()
) { ) {
settingsSections.forEach { (titleDescIcon, destination) -> settingsSections.forEach { (name, description, icon, destination) ->
SettingsListItem( SettingsListItem(
modifier = Modifier.clickable { navigate(destination) }, modifier = Modifier.clickable { navigate(destination) },
headlineContent = stringResource(titleDescIcon.first), headlineContent = stringResource(name),
supportingContent = stringResource(titleDescIcon.second), supportingContent = stringResource(description),
leadingContent = { Icon(titleDescIcon.third, null) } leadingContent = { Icon(icon, null) }
) )
} }
} }

View File

@ -5,36 +5,36 @@ import androidx.compose.animation.core.spring
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Cancel
import androidx.compose.material.icons.outlined.InstallMobile
import androidx.compose.material.icons.outlined.Update import androidx.compose.material.icons.outlined.Update
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import app.revanced.manager.BuildConfig
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.network.dto.ReVancedAsset import app.revanced.manager.network.dto.ReVancedAsset
import app.revanced.manager.ui.component.AppTopBar import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
import app.revanced.manager.ui.component.settings.Changelog import app.revanced.manager.ui.component.settings.Changelog
import app.revanced.manager.ui.viewmodel.UpdateViewModel import app.revanced.manager.ui.viewmodel.UpdateViewModel
import app.revanced.manager.ui.viewmodel.UpdateViewModel.State import app.revanced.manager.ui.viewmodel.UpdateViewModel.State
@ -52,38 +52,86 @@ fun UpdateScreen(
onBackClick: () -> Unit, onBackClick: () -> Unit,
vm: UpdateViewModel = koinViewModel() vm: UpdateViewModel = koinViewModel()
) { ) {
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
Scaffold( Scaffold(
topBar = { topBar = {
AppTopBar( AppTopBar(
title = stringResource(R.string.update), title = {
Column {
Text(stringResource(vm.state.title))
if (vm.state == State.DOWNLOADING) {
Text(
text = "${vm.downloadedSize.div(1000000)} MB / ${
vm.totalSize.div(1000000)
} MB (${vm.downloadProgress.times(100).toInt()}%)",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.outline
)
}
}
},
scrollBehavior = scrollBehavior,
onBackClick = onBackClick onBackClick = onBackClick
) )
} },
floatingActionButton = {
val buttonConfig = when (vm.state) {
State.CAN_DOWNLOAD -> Triple(
{ vm.downloadUpdate() },
R.string.download,
Icons.Outlined.InstallMobile
)
State.DOWNLOADING -> Triple(onBackClick, R.string.cancel, Icons.Outlined.Cancel)
State.CAN_INSTALL -> Triple(
{ vm.installUpdate() },
R.string.install_app,
Icons.Outlined.InstallMobile
)
else -> null
}
buttonConfig?.let { (onClick, textRes, icon) ->
HapticExtendedFloatingActionButton(
onClick = onClick::invoke,
icon = { Icon(icon, null) },
text = { Text(stringResource(textRes)) }
)
}
},
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
) { paddingValues -> ) { paddingValues ->
AnimatedVisibility(visible = vm.showInternetCheckDialog) {
MeteredDownloadConfirmationDialog(
onDismiss = { vm.showInternetCheckDialog = false },
onDownloadAnyways = { vm.downloadUpdate(true) }
)
}
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .padding(paddingValues),
.padding(paddingValues)
.padding(vertical = 16.dp, horizontal = 24.dp)
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(32.dp)
) { ) {
Header( if (vm.state == State.DOWNLOADING)
vm.state, LinearProgressIndicator(
vm.releaseInfo, progress = { vm.downloadProgress },
DownloadData(vm.downloadProgress, vm.downloadedSize, vm.totalSize) modifier = Modifier.fillMaxWidth(),
) )
vm.releaseInfo?.let { changelog ->
HorizontalDivider() AnimatedVisibility(visible = vm.showInternetCheckDialog) {
Changelog(changelog) MeteredDownloadConfirmationDialog(
} ?: Spacer(modifier = Modifier.weight(1f)) onDismiss = { vm.showInternetCheckDialog = false },
Buttons(vm.state, vm::downloadUpdate, vm::installUpdate, onBackClick) onDownloadAnyways = { vm.downloadUpdate(true) }
)
}
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(32.dp)
) {
vm.releaseInfo?.let { changelog ->
Changelog(changelog)
}
}
} }
} }
} }
@ -116,58 +164,6 @@ private fun MeteredDownloadConfirmationDialog(
) )
} }
@Composable
private fun Header(state: State, releaseInfo: ReVancedAsset?, downloadData: DownloadData) {
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
Text(
text = stringResource(state.title),
style = MaterialTheme.typography.headlineMedium
)
if (state == State.CAN_DOWNLOAD) {
Column {
Text(
text = stringResource(
id = R.string.current_version,
BuildConfig.VERSION_NAME
),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
releaseInfo?.version?.let {
Text(
text = stringResource(
R.string.new_version,
it.replace("v", "")
),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
} else if (state == State.DOWNLOADING) {
LinearProgressIndicator(
progress = { downloadData.downloadProgress },
modifier = Modifier.fillMaxWidth(),
)
Text(
text =
"${downloadData.downloadedSize.div(1000000)} MB / ${
downloadData.totalSize.div(
1000000
)
} MB (${
downloadData.downloadProgress.times(
100
).toInt()
}%)",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.outline,
modifier = Modifier.align(Alignment.CenterHorizontally)
)
}
}
}
@Composable @Composable
private fun ColumnScope.Changelog(releaseInfo: ReVancedAsset) { private fun ColumnScope.Changelog(releaseInfo: ReVancedAsset) {
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
@ -198,40 +194,4 @@ private fun ColumnScope.Changelog(releaseInfo: ReVancedAsset) {
publishDate = releaseInfo.createdAt.relativeTime(LocalContext.current) publishDate = releaseInfo.createdAt.relativeTime(LocalContext.current)
) )
} }
} }
@Composable
private fun Buttons(
state: State,
onDownloadClick: () -> Unit,
onInstallClick: () -> Unit,
onBackClick: () -> Unit
) {
Row(modifier = Modifier.fillMaxWidth()) {
if (state.showCancel) {
TextButton(
onClick = onBackClick,
) {
Text(text = stringResource(R.string.cancel))
}
}
Spacer(modifier = Modifier.weight(1f))
if (state == State.CAN_DOWNLOAD) {
Button(onClick = onDownloadClick) {
Text(text = stringResource(R.string.update))
}
} else if (state == State.CAN_INSTALL) {
Button(
onClick = onInstallClick
) {
Text(text = stringResource(R.string.install_app))
}
}
}
}
data class DownloadData(
val downloadProgress: Float,
val downloadedSize: Long,
val totalSize: Long
)

View File

@ -23,13 +23,26 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedCard import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.hideFromAccessibility
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import app.revanced.manager.BuildConfig import app.revanced.manager.BuildConfig
import app.revanced.manager.R import app.revanced.manager.R
@ -39,8 +52,10 @@ import app.revanced.manager.ui.component.ColumnWithScrollbar
import app.revanced.manager.ui.component.settings.SettingsListItem import app.revanced.manager.ui.component.settings.SettingsListItem
import app.revanced.manager.ui.model.navigation.Settings import app.revanced.manager.ui.model.navigation.Settings
import app.revanced.manager.ui.viewmodel.AboutViewModel import app.revanced.manager.ui.viewmodel.AboutViewModel
import app.revanced.manager.ui.viewmodel.AboutViewModel.Companion.DEVELOPER_OPTIONS_TAPS
import app.revanced.manager.ui.viewmodel.AboutViewModel.Companion.getSocialIcon import app.revanced.manager.ui.viewmodel.AboutViewModel.Companion.getSocialIcon
import app.revanced.manager.util.openUrl import app.revanced.manager.util.openUrl
import app.revanced.manager.util.toast
import com.google.accompanist.drawablepainter.rememberDrawablePainter import com.google.accompanist.drawablepainter.rememberDrawablePainter
import org.koin.androidx.compose.koinViewModel import org.koin.androidx.compose.koinViewModel
@ -105,7 +120,8 @@ fun AboutSettingsScreen(
} }
val listItems = listOfNotNull( val listItems = listOfNotNull(
Triple(stringResource(R.string.submit_feedback), Triple(
stringResource(R.string.submit_feedback),
stringResource(R.string.submit_feedback_description), stringResource(R.string.submit_feedback_description),
third = { third = {
context.openUrl("https://github.com/ReVanced/revanced-manager/issues/new/choose") context.openUrl("https://github.com/ReVanced/revanced-manager/issues/new/choose")
@ -113,12 +129,14 @@ fun AboutSettingsScreen(
Triple( Triple(
stringResource(R.string.contributors), stringResource(R.string.contributors),
stringResource(R.string.contributors_description), stringResource(R.string.contributors_description),
third = { navigate(Settings.Contributors) } third = nav@{
), if (!viewModel.isConnected) {
Triple( context.toast(context.getString(R.string.no_network_toast))
stringResource(R.string.developer_options), return@nav
stringResource(R.string.developer_options_description), }
third = { navigate(Settings.DeveloperOptions) }
navigate(Settings.Contributors)
}
), ),
Triple( Triple(
stringResource(R.string.opensource_licenses), stringResource(R.string.opensource_licenses),
@ -127,13 +145,49 @@ fun AboutSettingsScreen(
) )
) )
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
val snackbarHostState = remember { SnackbarHostState() }
val showDeveloperSettings by viewModel.showDeveloperSettings.getAsState()
var developerTaps by rememberSaveable { mutableIntStateOf(0) }
LaunchedEffect(developerTaps) {
if (developerTaps == 0) return@LaunchedEffect
if (showDeveloperSettings) {
snackbarHostState.showSnackbar(context.getString(R.string.developer_options_already_enabled))
developerTaps = 0
return@LaunchedEffect
}
val remaining = DEVELOPER_OPTIONS_TAPS - developerTaps
if (remaining > 0) {
snackbarHostState.showSnackbar(
context.getString(
R.string.developer_options_taps,
remaining
),
duration = SnackbarDuration.Long
)
} else if (remaining == 0) {
viewModel.showDeveloperSettings.update(true)
snackbarHostState.showSnackbar(context.getString(R.string.developer_options_enabled))
}
// Reset the counter
developerTaps = 0
}
Scaffold( Scaffold(
topBar = { topBar = {
AppTopBar( AppTopBar(
title = stringResource(R.string.about), title = stringResource(R.string.about),
scrollBehavior = scrollBehavior,
onBackClick = onBackClick onBackClick = onBackClick
) )
} },
snackbarHost = {
SnackbarHost(hostState = snackbarHostState)
},
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
) { paddingValues -> ) { paddingValues ->
ColumnWithScrollbar( ColumnWithScrollbar(
modifier = Modifier modifier = Modifier
@ -143,9 +197,11 @@ fun AboutSettingsScreen(
verticalArrangement = Arrangement.spacedBy(16.dp) verticalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
Image( Image(
modifier = Modifier.padding(top = 16.dp), modifier = Modifier
.padding(top = 16.dp)
.clickable { developerTaps += 1 },
painter = icon, painter = icon,
contentDescription = null contentDescription = stringResource(R.string.app_name)
) )
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
@ -153,7 +209,11 @@ fun AboutSettingsScreen(
) { ) {
Text( Text(
stringResource(R.string.app_name), stringResource(R.string.app_name),
style = MaterialTheme.typography.headlineSmall style = MaterialTheme.typography.headlineSmall,
modifier = Modifier.semantics {
// Icon already has this information for the purpose of being clickable.
hideFromAccessibility()
}
) )
Text( Text(
text = stringResource(R.string.version) + " " + BuildConfig.VERSION_NAME + " (" + BuildConfig.VERSION_CODE + ")", text = stringResource(R.string.version) + " " + BuildConfig.VERSION_NAME + " (" + BuildConfig.VERSION_CODE + ")",

View File

@ -10,14 +10,33 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Api import androidx.compose.material.icons.outlined.Api
import androidx.compose.material.icons.outlined.Restore import androidx.compose.material.icons.outlined.Restore
import androidx.compose.material3.* import androidx.compose.material3.AlertDialog
import androidx.compose.runtime.* import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
@ -53,14 +72,17 @@ fun AdvancedSettingsScreen(
activityManager.largeMemoryClass activityManager.largeMemoryClass
) )
} }
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
Scaffold( Scaffold(
topBar = { topBar = {
AppTopBar( AppTopBar(
title = stringResource(R.string.advanced), title = stringResource(R.string.advanced),
scrollBehavior = scrollBehavior,
onBackClick = onBackClick onBackClick = onBackClick
) )
} },
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
) { paddingValues -> ) { paddingValues ->
ColumnWithScrollbar( ColumnWithScrollbar(
modifier = Modifier modifier = Modifier
@ -90,20 +112,6 @@ fun AdvancedSettingsScreen(
} }
) )
GroupHeader(stringResource(R.string.patcher))
BooleanItem(
preference = vm.prefs.useProcessRuntime,
coroutineScope = vm.viewModelScope,
headline = R.string.process_runtime,
description = R.string.process_runtime_description,
)
IntegerItem(
preference = vm.prefs.patcherProcessMemoryLimit,
coroutineScope = vm.viewModelScope,
headline = R.string.process_runtime_memory_limit,
description = R.string.process_runtime_memory_limit_description,
)
GroupHeader(stringResource(R.string.safeguards)) GroupHeader(stringResource(R.string.safeguards))
SafeguardBooleanItem( SafeguardBooleanItem(
preference = vm.prefs.disablePatchVersionCompatCheck, preference = vm.prefs.disablePatchVersionCompatCheck,
@ -134,6 +142,20 @@ fun AdvancedSettingsScreen(
confirmationText = R.string.patch_selection_safeguard_confirmation confirmationText = R.string.patch_selection_safeguard_confirmation
) )
GroupHeader(stringResource(R.string.patcher))
BooleanItem(
preference = vm.prefs.useProcessRuntime,
coroutineScope = vm.viewModelScope,
headline = R.string.process_runtime,
description = R.string.process_runtime_description,
)
IntegerItem(
preference = vm.prefs.patcherProcessMemoryLimit,
coroutineScope = vm.viewModelScope,
headline = R.string.process_runtime_memory_limit,
description = R.string.process_runtime_memory_limit_description,
)
GroupHeader(stringResource(R.string.debugging)) GroupHeader(stringResource(R.string.debugging))
val exportDebugLogsLauncher = val exportDebugLogsLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) { rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) {

View File

@ -25,11 +25,14 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
@ -48,18 +51,22 @@ import org.koin.androidx.compose.koinViewModel
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun ContributorScreen( fun ContributorSettingsScreen(
onBackClick: () -> Unit, onBackClick: () -> Unit,
viewModel: ContributorViewModel = koinViewModel() viewModel: ContributorViewModel = koinViewModel()
) { ) {
val repositories = viewModel.repositories val repositories = viewModel.repositories
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
Scaffold( Scaffold(
topBar = { topBar = {
AppTopBar( AppTopBar(
title = stringResource(R.string.contributors), title = stringResource(R.string.contributors),
scrollBehavior = scrollBehavior,
onBackClick = onBackClick onBackClick = onBackClick
) )
}, },
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
) { paddingValues -> ) { paddingValues ->
LazyColumnWithScrollbar( LazyColumnWithScrollbar(
modifier = Modifier modifier = Modifier
@ -90,7 +97,14 @@ fun ContributorScreen(
) )
} }
} }
} ?: item { LoadingIndicator() } } ?: item {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center
) {
LoadingIndicator()
}
}
} }
} }
} }

View File

@ -5,31 +5,49 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.ui.component.AppTopBar import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.GroupHeader import app.revanced.manager.ui.component.GroupHeader
import app.revanced.manager.ui.component.settings.BooleanItem
import app.revanced.manager.ui.component.settings.SettingsListItem import app.revanced.manager.ui.component.settings.SettingsListItem
import app.revanced.manager.ui.viewmodel.DeveloperOptionsViewModel import app.revanced.manager.ui.viewmodel.DeveloperOptionsViewModel
import org.koin.androidx.compose.koinViewModel import org.koin.androidx.compose.koinViewModel
import org.koin.compose.koinInject
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun DeveloperOptionsScreen( fun DeveloperSettingsScreen(
onBackClick: () -> Unit, onBackClick: () -> Unit,
vm: DeveloperOptionsViewModel = koinViewModel() vm: DeveloperOptionsViewModel = koinViewModel()
) { ) {
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
val prefs: PreferencesManager = koinInject()
Scaffold( Scaffold(
topBar = { topBar = {
AppTopBar( AppTopBar(
title = stringResource(R.string.developer_options), title = stringResource(R.string.developer_options),
scrollBehavior = scrollBehavior,
onBackClick = onBackClick onBackClick = onBackClick
) )
} },
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
) { paddingValues -> ) { paddingValues ->
Column(modifier = Modifier.padding(paddingValues)) { Column(modifier = Modifier.padding(paddingValues)) {
GroupHeader(stringResource(R.string.manager))
BooleanItem(
preference = prefs.showDeveloperSettings,
headline = R.string.developer_options,
description = R.string.developer_options_description,
)
GroupHeader(stringResource(R.string.patch_bundles_section)) GroupHeader(stringResource(R.string.patch_bundles_section))
SettingsListItem( SettingsListItem(
headlineContent = stringResource(R.string.patch_bundles_force_download), headlineContent = stringResource(R.string.patch_bundles_force_download),

View File

@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@ -17,9 +18,11 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults
import androidx.compose.material3.pulltorefresh.pullToRefresh import androidx.compose.material3.pulltorefresh.pullToRefresh
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@ -28,6 +31,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -40,6 +44,7 @@ import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.ExceptionViewerDialog import app.revanced.manager.ui.component.ExceptionViewerDialog
import app.revanced.manager.ui.component.GroupHeader import app.revanced.manager.ui.component.GroupHeader
import app.revanced.manager.ui.component.LazyColumnWithScrollbar import app.revanced.manager.ui.component.LazyColumnWithScrollbar
import app.revanced.manager.ui.component.ConfirmDialog
import app.revanced.manager.ui.component.haptics.HapticCheckbox import app.revanced.manager.ui.component.haptics.HapticCheckbox
import app.revanced.manager.ui.component.settings.SettingsListItem import app.revanced.manager.ui.component.settings.SettingsListItem
import app.revanced.manager.ui.viewmodel.DownloadsViewModel import app.revanced.manager.ui.viewmodel.DownloadsViewModel
@ -55,21 +60,35 @@ fun DownloadsSettingsScreen(
val pullRefreshState = rememberPullToRefreshState() val pullRefreshState = rememberPullToRefreshState()
val downloadedApps by viewModel.downloadedApps.collectAsStateWithLifecycle(emptyList()) val downloadedApps by viewModel.downloadedApps.collectAsStateWithLifecycle(emptyList())
val pluginStates by viewModel.downloaderPluginStates.collectAsStateWithLifecycle() val pluginStates by viewModel.downloaderPluginStates.collectAsStateWithLifecycle()
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
var showDeleteConfirmationDialog by rememberSaveable { mutableStateOf(false) }
if (showDeleteConfirmationDialog) {
ConfirmDialog(
onDismiss = { showDeleteConfirmationDialog = false },
onConfirm = { viewModel.deleteApps() },
title = stringResource(R.string.downloader_plugin_delete_apps_title),
description = stringResource(R.string.downloader_plugin_delete_apps_description),
icon = Icons.Outlined.Delete
)
}
Scaffold( Scaffold(
topBar = { topBar = {
AppTopBar( AppTopBar(
title = stringResource(R.string.downloads), title = stringResource(R.string.downloads),
scrollBehavior = scrollBehavior,
onBackClick = onBackClick, onBackClick = onBackClick,
actions = { actions = {
if (viewModel.appSelection.isNotEmpty()) { if (viewModel.appSelection.isNotEmpty()) {
IconButton(onClick = { viewModel.deleteApps() }) { IconButton(onClick = { showDeleteConfirmationDialog = true }) {
Icon(Icons.Default.Delete, stringResource(R.string.delete)) Icon(Icons.Default.Delete, stringResource(R.string.delete))
} }
} }
} }
) )
} },
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
) { paddingValues -> ) { paddingValues ->
Box( Box(
contentAlignment = Alignment.TopCenter, contentAlignment = Alignment.TopCenter,

View File

@ -13,6 +13,8 @@ import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@ -20,6 +22,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import app.revanced.manager.R import app.revanced.manager.R
@ -51,14 +54,17 @@ fun GeneralSettingsScreen(
onConfirm = { viewModel.setTheme(it) } onConfirm = { viewModel.setTheme(it) }
) )
} }
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
Scaffold( Scaffold(
topBar = { topBar = {
AppTopBar( AppTopBar(
title = stringResource(R.string.general), title = stringResource(R.string.general),
scrollBehavior = scrollBehavior,
onBackClick = onBackClick onBackClick = onBackClick
) )
} },
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
) { paddingValues -> ) { paddingValues ->
ColumnWithScrollbar( ColumnWithScrollbar(
modifier = Modifier modifier = Modifier

View File

@ -13,7 +13,17 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Key import androidx.compose.material.icons.outlined.Key
import androidx.compose.material3.* import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@ -22,6 +32,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
@ -37,6 +48,7 @@ import app.revanced.manager.ui.component.bundle.BundleSelector
import app.revanced.manager.ui.component.settings.SettingsListItem import app.revanced.manager.ui.component.settings.SettingsListItem
import app.revanced.manager.ui.viewmodel.ImportExportViewModel import app.revanced.manager.ui.viewmodel.ImportExportViewModel
import app.revanced.manager.util.toast import app.revanced.manager.util.toast
import app.revanced.manager.util.uiSafe
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.androidx.compose.koinViewModel import org.koin.androidx.compose.koinViewModel
@ -86,27 +98,56 @@ fun ImportExportSettingsScreen(
onDismissRequest = vm::cancelKeystoreImport, onDismissRequest = vm::cancelKeystoreImport,
onSubmit = { cn, pass -> onSubmit = { cn, pass ->
vm.viewModelScope.launch { vm.viewModelScope.launch {
val result = vm.tryKeystoreImport(cn, pass) uiSafe(context, R.string.failed_to_import_keystore, "Failed to import keystore") {
if (!result) context.toast(context.getString(R.string.import_keystore_wrong_credentials)) val result = vm.tryKeystoreImport(cn, pass)
if (!result) context.toast(context.getString(R.string.import_keystore_wrong_credentials))
}
} }
} }
) )
} }
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
Scaffold( Scaffold(
topBar = { topBar = {
AppTopBar( AppTopBar(
title = stringResource(R.string.import_export), title = stringResource(R.string.import_export),
scrollBehavior = scrollBehavior,
onBackClick = onBackClick onBackClick = onBackClick
) )
} },
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
) { paddingValues -> ) { paddingValues ->
ColumnWithScrollbar( ColumnWithScrollbar(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(paddingValues) .padding(paddingValues)
) { ) {
GroupHeader(stringResource(R.string.signing)) var showPackageSelector by rememberSaveable {
mutableStateOf(false)
}
var showBundleSelector by rememberSaveable {
mutableStateOf(false)
}
if (showPackageSelector) {
PackageSelector(packages = packagesWithOptions) { selected ->
selected?.let(vm::resetOptionsForPackage)
showPackageSelector = false
}
}
if (showBundleSelector) {
BundleSelector(bundles = patchBundles) { bundle ->
bundle?.let(vm::clearOptionsForBundle)
showBundleSelector = false
}
}
GroupHeader(stringResource(R.string.import_))
GroupItem( GroupItem(
onClick = { onClick = {
importKeystoreLauncher.launch("*/*") importKeystoreLauncher.launch("*/*")
@ -114,6 +155,13 @@ fun ImportExportSettingsScreen(
headline = R.string.import_keystore, headline = R.string.import_keystore,
description = R.string.import_keystore_description description = R.string.import_keystore_description
) )
GroupItem(
onClick = vm::importSelection,
headline = R.string.import_patch_selection,
description = R.string.import_patch_selection_description
)
GroupHeader(stringResource(R.string.export))
GroupItem( GroupItem(
onClick = { onClick = {
if (!vm.canExport()) { if (!vm.canExport()) {
@ -125,54 +173,25 @@ fun ImportExportSettingsScreen(
headline = R.string.export_keystore, headline = R.string.export_keystore,
description = R.string.export_keystore_description description = R.string.export_keystore_description
) )
GroupItem(
onClick = vm::regenerateKeystore,
headline = R.string.regenerate_keystore,
description = R.string.regenerate_keystore_description
)
GroupHeader(stringResource(R.string.patches))
GroupItem(
onClick = vm::importSelection,
headline = R.string.import_patch_selection,
description = R.string.import_patch_selection_description
)
GroupItem( GroupItem(
onClick = vm::exportSelection, onClick = vm::exportSelection,
headline = R.string.export_patch_selection, headline = R.string.export_patch_selection,
description = R.string.export_patch_selection_description description = R.string.export_patch_selection_description
) )
// TODO: allow resetting selection for specific bundle or package name.
GroupHeader(stringResource(R.string.reset))
GroupItem( GroupItem(
onClick = vm::resetSelection, onClick = vm::regenerateKeystore,
headline = R.string.regenerate_keystore,
description = R.string.regenerate_keystore_description
)
GroupItem(
onClick = vm::resetSelection, // TODO: allow resetting selection for specific bundle or package name.
headline = R.string.reset_patch_selection, headline = R.string.reset_patch_selection,
description = R.string.reset_patch_selection_description description = R.string.reset_patch_selection_description
) )
var showPackageSelector by rememberSaveable {
mutableStateOf(false)
}
var showBundleSelector by rememberSaveable {
mutableStateOf(false)
}
if (showPackageSelector)
PackageSelector(packages = packagesWithOptions) { selected ->
selected?.let(vm::resetOptionsForPackage)
showPackageSelector = false
}
if (showBundleSelector)
BundleSelector(bundles = patchBundles) { bundle ->
bundle?.let(vm::clearOptionsForBundle)
showBundleSelector = false
}
// TODO: patch options import/export.
GroupItem( GroupItem(
onClick = vm::resetOptions, onClick = vm::resetOptions, // TODO: patch options import/export.
headline = R.string.patch_options_reset_all, headline = R.string.patch_options_reset_all,
description = R.string.patch_options_reset_all_description, description = R.string.patch_options_reset_all_description,
) )
@ -181,12 +200,13 @@ fun ImportExportSettingsScreen(
headline = R.string.patch_options_reset_package, headline = R.string.patch_options_reset_package,
description = R.string.patch_options_reset_package_description description = R.string.patch_options_reset_package_description
) )
if (patchBundles.size > 1) if (patchBundles.size > 1) {
GroupItem( GroupItem(
onClick = { showBundleSelector = true }, onClick = { showBundleSelector = true },
headline = R.string.patch_options_reset_bundle, headline = R.string.patch_options_reset_bundle,
description = R.string.patch_options_reset_bundle_description, description = R.string.patch_options_reset_bundle_description,
) )
}
} }
} }
} }

View File

@ -18,7 +18,7 @@ import com.mikepenz.aboutlibraries.ui.compose.LibraryDefaults
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun LicensesScreen( fun LicensesSettingsScreen(
onBackClick: () -> Unit, onBackClick: () -> Unit,
) { ) {
AppScaffold( AppScaffold(

View File

@ -7,9 +7,12 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -24,17 +27,21 @@ import org.koin.androidx.compose.koinViewModel
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun ChangelogsScreen( fun ChangelogsSettingsScreen(
onBackClick: () -> Unit, onBackClick: () -> Unit,
vm: ChangelogsViewModel = koinViewModel() vm: ChangelogsViewModel = koinViewModel()
) { ) {
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
Scaffold( Scaffold(
topBar = { topBar = {
AppTopBar( AppTopBar(
title = stringResource(R.string.changelog), title = stringResource(R.string.changelog),
scrollBehavior = scrollBehavior,
onBackClick = onBackClick onBackClick = onBackClick
) )
} },
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
) { paddingValues -> ) { paddingValues ->
ColumnWithScrollbar( ColumnWithScrollbar(
modifier = Modifier modifier = Modifier

View File

@ -5,9 +5,13 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.ui.component.AppTopBar import app.revanced.manager.ui.component.AppTopBar
@ -15,6 +19,7 @@ import app.revanced.manager.ui.component.ColumnWithScrollbar
import app.revanced.manager.ui.component.settings.BooleanItem import app.revanced.manager.ui.component.settings.BooleanItem
import app.revanced.manager.ui.component.settings.SettingsListItem import app.revanced.manager.ui.component.settings.SettingsListItem
import app.revanced.manager.ui.viewmodel.UpdatesSettingsViewModel import app.revanced.manager.ui.viewmodel.UpdatesSettingsViewModel
import app.revanced.manager.util.toast
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.androidx.compose.koinViewModel import org.koin.androidx.compose.koinViewModel
@ -26,15 +31,19 @@ fun UpdatesSettingsScreen(
onUpdateClick: () -> Unit, onUpdateClick: () -> Unit,
vm: UpdatesSettingsViewModel = koinViewModel(), vm: UpdatesSettingsViewModel = koinViewModel(),
) { ) {
val context = LocalContext.current
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
Scaffold( Scaffold(
topBar = { topBar = {
AppTopBar( AppTopBar(
title = stringResource(R.string.updates), title = stringResource(R.string.updates),
scrollBehavior = scrollBehavior,
onBackClick = onBackClick onBackClick = onBackClick
) )
} },
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
) { paddingValues -> ) { paddingValues ->
ColumnWithScrollbar( ColumnWithScrollbar(
modifier = Modifier modifier = Modifier
@ -44,6 +53,10 @@ fun UpdatesSettingsScreen(
SettingsListItem( SettingsListItem(
modifier = Modifier.clickable { modifier = Modifier.clickable {
coroutineScope.launch { coroutineScope.launch {
if (!vm.isConnected) {
context.toast(context.getString(R.string.no_network_toast))
return@launch
}
if (vm.checkForUpdates()) onUpdateClick() if (vm.checkForUpdates()) onUpdateClick()
} }
}, },
@ -52,7 +65,13 @@ fun UpdatesSettingsScreen(
) )
SettingsListItem( SettingsListItem(
modifier = Modifier.clickable(onClick = onChangelogClick), modifier = Modifier.clickable {
if (!vm.isConnected) {
context.toast(context.getString(R.string.no_network_toast))
return@clickable
}
onChangelogClick()
},
headlineContent = stringResource(R.string.changelog), headlineContent = stringResource(R.string.changelog),
supportingContent = stringResource( supportingContent = stringResource(
R.string.changelog_description R.string.changelog_description

View File

@ -7,6 +7,8 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import app.revanced.manager.data.platform.NetworkInfo
import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.network.api.ReVancedAPI import app.revanced.manager.network.api.ReVancedAPI
import app.revanced.manager.network.dto.ReVancedDonationLink import app.revanced.manager.network.dto.ReVancedDonationLink
import app.revanced.manager.network.dto.ReVancedSocial import app.revanced.manager.network.dto.ReVancedSocial
@ -23,16 +25,27 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
class AboutViewModel(private val reVancedAPI: ReVancedAPI) : ViewModel() { class AboutViewModel(
private val reVancedAPI: ReVancedAPI,
private val network: NetworkInfo,
prefs: PreferencesManager,
) : ViewModel() {
var socials by mutableStateOf(emptyList<ReVancedSocial>()) var socials by mutableStateOf(emptyList<ReVancedSocial>())
private set private set
var contact by mutableStateOf<String?>(null) var contact by mutableStateOf<String?>(null)
private set private set
var donate by mutableStateOf<String?>(null) var donate by mutableStateOf<String?>(null)
private set private set
val isConnected: Boolean
get() = network.isConnected()
val showDeveloperSettings = prefs.showDeveloperSettings
init { init {
viewModelScope.launch { viewModelScope.launch {
if (!isConnected) {
return@launch
}
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
reVancedAPI.getInfo("https://api.revanced.app").getOrNull() reVancedAPI.getInfo("https://api.revanced.app").getOrNull()
}?.let { }?.let {
@ -44,6 +57,8 @@ class AboutViewModel(private val reVancedAPI: ReVancedAPI) : ViewModel() {
} }
companion object { companion object {
const val DEVELOPER_OPTIONS_TAPS = 5
private val socialIcons = mapOf( private val socialIcons = mapOf(
"Discord" to FontAwesomeIcons.Brands.Discord, "Discord" to FontAwesomeIcons.Brands.Discord,
"GitHub" to FontAwesomeIcons.Brands.Github, "GitHub" to FontAwesomeIcons.Brands.Github,

View File

@ -24,9 +24,7 @@ import app.revanced.manager.network.api.ReVancedAPI
import app.revanced.manager.util.PM import app.revanced.manager.util.PM
import app.revanced.manager.util.toast import app.revanced.manager.util.toast
import app.revanced.manager.util.uiSafe import app.revanced.manager.util.uiSafe
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -58,19 +56,13 @@ class DashboardViewModel(
var updatedManagerVersion: String? by mutableStateOf(null) var updatedManagerVersion: String? by mutableStateOf(null)
private set private set
val showBatteryOptimizationsWarningFlow = flow { var showBatteryOptimizationsWarning by mutableStateOf(false)
while (true) { private set
// 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 { init {
viewModelScope.launch { viewModelScope.launch {
checkForManagerUpdates() checkForManagerUpdates()
updateBatteryOptimizationsWarning()
} }
} }
@ -90,6 +82,10 @@ class DashboardViewModel(
} }
} }
fun updateBatteryOptimizationsWarning() {
showBatteryOptimizationsWarning = !powerManager.isIgnoringBatteryOptimizations(app.packageName)
}
fun setShowManagerUpdateDialogOnLaunch(value: Boolean) { fun setShowManagerUpdateDialogOnLaunch(value: Boolean) {
viewModelScope.launch { viewModelScope.launch {
prefs.showManagerUpdateDialogOnLaunch.update(value) prefs.showManagerUpdateDialogOnLaunch.update(value)

View File

@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import app.revanced.manager.domain.manager.PreferencesManager import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.ui.theme.Theme import app.revanced.manager.ui.theme.Theme
import app.revanced.manager.util.resetListItemColorsCached
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class GeneralSettingsViewModel( class GeneralSettingsViewModel(
@ -11,5 +12,6 @@ class GeneralSettingsViewModel(
) : ViewModel() { ) : ViewModel() {
fun setTheme(theme: Theme) = viewModelScope.launch { fun setTheme(theme: Theme) = viewModelScope.launch {
prefs.theme.update(theme) prefs.theme.update(theme)
resetListItemColorsCached()
} }
} }

View File

@ -69,25 +69,27 @@ class ImportExportViewModel(
} }
fun startKeystoreImport(content: Uri) = viewModelScope.launch { fun startKeystoreImport(content: Uri) = viewModelScope.launch {
val path = withContext(Dispatchers.IO) { uiSafe(app, R.string.failed_to_import_keystore, "Failed to import keystore") {
File.createTempFile("signing", "ks", app.cacheDir).toPath().also { val path = withContext(Dispatchers.IO) {
Files.copy( File.createTempFile("signing", "ks", app.cacheDir).toPath().also {
contentResolver.openInputStream(content)!!, Files.copy(
it, contentResolver.openInputStream(content)!!,
StandardCopyOption.REPLACE_EXISTING it,
) StandardCopyOption.REPLACE_EXISTING
} )
}
aliases.forEach { alias ->
knownPasswords.forEach { pass ->
if (tryKeystoreImport(alias, pass, path)) {
return@launch
} }
} }
}
keystoreImportPath = path aliases.forEach { alias ->
knownPasswords.forEach { pass ->
if (tryKeystoreImport(alias, pass, path)) {
return@launch
}
}
}
keystoreImportPath = path
}
} }
fun cancelKeystoreImport() { fun cancelKeystoreImport() {

View File

@ -83,7 +83,9 @@ class PatcherViewModel(
private val savedStateHandle: SavedStateHandle = get() private val savedStateHandle: SavedStateHandle = get()
private var installedApp: InstalledApp? = null private var installedApp: InstalledApp? = null
val packageName = input.selectedApp.packageName private val selectedApp = input.selectedApp
val packageName = selectedApp.packageName
val version = selectedApp.version
var installedPackageName by savedStateHandle.saveable( var installedPackageName by savedStateHandle.saveable(
key = "installedPackageName", key = "installedPackageName",

View File

@ -3,11 +3,11 @@ package app.revanced.manager.ui.viewmodel
import android.app.Application import android.app.Application
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.setValue
import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.Saver import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.SnapshotStateMap import androidx.compose.runtime.snapshots.SnapshotStateMap
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
@ -30,15 +30,20 @@ import app.revanced.manager.util.saver.persistentMapSaver
import app.revanced.manager.util.saver.persistentSetSaver import app.revanced.manager.util.saver.persistentSetSaver
import app.revanced.manager.util.saver.snapshotStateMapSaver import app.revanced.manager.util.saver.snapshotStateMapSaver
import app.revanced.manager.util.toast import app.revanced.manager.util.toast
import kotlinx.collections.immutable.PersistentMap
import kotlinx.collections.immutable.PersistentSet
import kotlinx.collections.immutable.persistentMapOf
import kotlinx.collections.immutable.persistentSetOf
import kotlinx.collections.immutable.toPersistentMap
import kotlinx.collections.immutable.toPersistentSet
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.get import org.koin.core.component.get
import kotlinx.collections.immutable.*
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
@OptIn(SavedStateHandleSaveableApi::class) @OptIn(SavedStateHandleSaveableApi::class)
class PatchesSelectorViewModel(input: SelectedApplicationInfo.PatchesSelector.ViewModelParams) : class PatchesSelectorViewModel(input: SelectedApplicationInfo.PatchesSelector.ViewModelParams) :
@ -208,8 +213,8 @@ class PatchesSelectorViewModel(input: SelectedApplicationInfo.PatchesSelector.Vi
compatibleVersions.clear() compatibleVersions.clear()
} }
fun openUnsupportedDialog(unsupportedPatch: PatchInfo) { fun openIncompatibleDialog(incompatiblePatch: PatchInfo) {
compatibleVersions.addAll(unsupportedPatch.compatiblePackages?.find { it.packageName == packageName }?.versions.orEmpty()) compatibleVersions.addAll(incompatiblePatch.compatiblePackages?.find { it.packageName == packageName }?.versions.orEmpty())
} }
fun toggleFlag(flag: Int) { fun toggleFlag(flag: Int) {
@ -217,7 +222,7 @@ class PatchesSelectorViewModel(input: SelectedApplicationInfo.PatchesSelector.Vi
} }
companion object { companion object {
const val SHOW_UNSUPPORTED = 1 // 2^0 const val SHOW_INCOMPATIBLE = 1 // 2^0
const val SHOW_UNIVERSAL = 2 // 2^1 const val SHOW_UNIVERSAL = 2 // 2^1
private val optionsSaver: Saver<PersistentOptions, Options> = snapshotStateMapSaver( private val optionsSaver: Saver<PersistentOptions, Options> = snapshotStateMapSaver(

View File

@ -30,14 +30,13 @@ import app.revanced.manager.domain.repository.PatchOptionsRepository
import app.revanced.manager.domain.repository.PatchSelectionRepository import app.revanced.manager.domain.repository.PatchSelectionRepository
import app.revanced.manager.network.downloader.LoadedDownloaderPlugin import app.revanced.manager.network.downloader.LoadedDownloaderPlugin
import app.revanced.manager.network.downloader.ParceledDownloaderData 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.GetScope
import app.revanced.manager.plugin.downloader.PluginHostApi import app.revanced.manager.plugin.downloader.PluginHostApi
import app.revanced.manager.plugin.downloader.UserInteractionException import app.revanced.manager.plugin.downloader.UserInteractionException
import app.revanced.manager.ui.model.BundleInfo import app.revanced.manager.ui.model.BundleInfo
import app.revanced.manager.ui.model.BundleInfo.Extensions.bundleInfoFlow 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.BundleInfo.Extensions.requiredOptionsSet
import app.revanced.manager.ui.model.BundleInfo.Extensions.toPatchSelection
import app.revanced.manager.ui.model.SelectedApp import app.revanced.manager.ui.model.SelectedApp
import app.revanced.manager.ui.model.navigation.Patcher import app.revanced.manager.ui.model.navigation.Patcher
import app.revanced.manager.ui.model.navigation.SelectedApplicationInfo import app.revanced.manager.ui.model.navigation.SelectedApplicationInfo
@ -275,25 +274,25 @@ class SelectedAppInfoViewModel(
) )
suspend fun getPatcherParams(): Patcher.ViewModelParams { suspend fun getPatcherParams(): Patcher.ViewModelParams {
val allowUnsupported = prefs.disablePatchVersionCompatCheck.get() val allowIncompatible = prefs.disablePatchVersionCompatCheck.get()
val bundles = bundleInfoFlow.first() val bundles = bundleInfoFlow.first()
return Patcher.ViewModelParams( return Patcher.ViewModelParams(
selectedApp, selectedApp,
getPatches(bundles, allowUnsupported), getPatches(bundles, allowIncompatible),
getOptionsFiltered(bundles) getOptionsFiltered(bundles)
) )
} }
fun getOptionsFiltered(bundles: List<BundleInfo>) = options.filtered(bundles) fun getOptionsFiltered(bundles: List<BundleInfo>) = options.filtered(bundles)
fun getPatches(bundles: List<BundleInfo>, allowUnsupported: Boolean) = fun getPatches(bundles: List<BundleInfo>, allowIncompatible: Boolean) =
selectionState.patches(bundles, allowUnsupported) selectionState.patches(bundles, allowIncompatible)
fun getCustomPatches( fun getCustomPatches(
bundles: List<BundleInfo>, bundles: List<BundleInfo>,
allowUnsupported: Boolean allowIncompatible: Boolean
): PatchSelection? = ): PatchSelection? =
(selectionState as? SelectionState.Customized)?.patches(bundles, allowUnsupported) (selectionState as? SelectionState.Customized)?.patches(bundles, allowIncompatible)
fun updateConfiguration(selection: PatchSelection?, options: Options) = viewModelScope.launch { fun updateConfiguration(selection: PatchSelection?, options: Options) = viewModelScope.launch {
val bundles = bundleInfoFlow.first() val bundles = bundleInfoFlow.first()
@ -343,13 +342,13 @@ class SelectedAppInfoViewModel(
} }
private sealed interface SelectionState : Parcelable { private sealed interface SelectionState : Parcelable {
fun patches(bundles: List<BundleInfo>, allowUnsupported: Boolean): PatchSelection fun patches(bundles: List<BundleInfo>, allowIncompatible: Boolean): PatchSelection
@Parcelize @Parcelize
data class Customized(val patchSelection: PatchSelection) : SelectionState { data class Customized(val patchSelection: PatchSelection) : SelectionState {
override fun patches(bundles: List<BundleInfo>, allowUnsupported: Boolean) = override fun patches(bundles: List<BundleInfo>, allowIncompatible: Boolean) =
bundles.toPatchSelection( bundles.toPatchSelection(
allowUnsupported allowIncompatible
) { uid, patch -> ) { uid, patch ->
patchSelection[uid]?.contains(patch.name) ?: false patchSelection[uid]?.contains(patch.name) ?: false
} }
@ -357,8 +356,8 @@ private sealed interface SelectionState : Parcelable {
@Parcelize @Parcelize
data object Default : SelectionState { data object Default : SelectionState {
override fun patches(bundles: List<BundleInfo>, allowUnsupported: Boolean) = override fun patches(bundles: List<BundleInfo>, allowIncompatible: Boolean) =
bundles.toPatchSelection(allowUnsupported) { _, patch -> patch.include } bundles.toPatchSelection(allowIncompatible) { _, patch -> patch.include }
} }
} }

View File

@ -9,6 +9,7 @@ import android.content.pm.PackageInstaller
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
@ -42,9 +43,9 @@ class UpdateViewModel(
private val networkInfo: NetworkInfo by inject() private val networkInfo: NetworkInfo by inject()
private val fs: Filesystem by inject() private val fs: Filesystem by inject()
var downloadedSize by mutableStateOf(0L) var downloadedSize by mutableLongStateOf(0L)
private set private set
var totalSize by mutableStateOf(0L) var totalSize by mutableLongStateOf(0L)
private set private set
val downloadProgress by derivedStateOf { val downloadProgress by derivedStateOf {
if (downloadedSize == 0L || totalSize == 0L) return@derivedStateOf 0f if (downloadedSize == 0L || totalSize == 0L) return@derivedStateOf 0f
@ -89,7 +90,7 @@ class UpdateViewModel(
totalSize = contentLength totalSize = contentLength
} }
} }
state = State.CAN_INSTALL installUpdate()
} }
} }
} }
@ -108,14 +109,19 @@ class UpdateViewModel(
val extra = val extra =
intent.getStringExtra(InstallService.EXTRA_INSTALL_STATUS_MESSAGE)!! intent.getStringExtra(InstallService.EXTRA_INSTALL_STATUS_MESSAGE)!!
if (pmStatus == PackageInstaller.STATUS_SUCCESS) { when(pmStatus) {
app.toast(app.getString(R.string.install_app_success)) PackageInstaller.STATUS_SUCCESS -> {
state = State.SUCCESS app.toast(app.getString(R.string.install_app_success))
} else { state = State.SUCCESS
state = State.FAILED }
// TODO: handle install fail with a popup PackageInstaller.STATUS_FAILURE_ABORTED -> {
installError = extra state = State.CAN_INSTALL
app.toast(app.getString(R.string.install_app_fail, extra)) }
else -> {
app.toast(app.getString(R.string.install_app_fail, extra))
installError = extra
state = State.FAILED
}
} }
} }
} }
@ -135,10 +141,10 @@ class UpdateViewModel(
location.delete() location.delete()
} }
enum class State(@StringRes val title: Int, val showCancel: Boolean = false) { enum class State(@StringRes val title: Int) {
CAN_DOWNLOAD(R.string.update_available), CAN_DOWNLOAD(R.string.update_available),
DOWNLOADING(R.string.downloading_manager_update, true), DOWNLOADING(R.string.downloading_manager_update),
CAN_INSTALL(R.string.ready_to_install_update, true), CAN_INSTALL(R.string.ready_to_install_update),
INSTALLING(R.string.installing_manager_update), INSTALLING(R.string.installing_manager_update),
FAILED(R.string.install_update_manager_failed), FAILED(R.string.install_update_manager_failed),
SUCCESS(R.string.update_completed) SUCCESS(R.string.update_completed)

View File

@ -3,6 +3,7 @@ package app.revanced.manager.ui.viewmodel
import android.app.Application import android.app.Application
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.data.platform.NetworkInfo
import app.revanced.manager.domain.manager.PreferencesManager import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.network.api.ReVancedAPI import app.revanced.manager.network.api.ReVancedAPI
import app.revanced.manager.util.toast import app.revanced.manager.util.toast
@ -12,10 +13,14 @@ class UpdatesSettingsViewModel(
prefs: PreferencesManager, prefs: PreferencesManager,
private val app: Application, private val app: Application,
private val reVancedAPI: ReVancedAPI, private val reVancedAPI: ReVancedAPI,
private val network: NetworkInfo,
) : ViewModel() { ) : ViewModel() {
val managerAutoUpdates = prefs.managerAutoUpdates val managerAutoUpdates = prefs.managerAutoUpdates
val showManagerUpdateDialogOnLaunch = prefs.showManagerUpdateDialogOnLaunch val showManagerUpdateDialogOnLaunch = prefs.showManagerUpdateDialogOnLaunch
val isConnected: Boolean
get() = network.isConnected()
suspend fun checkForUpdates(): Boolean { suspend fun checkForUpdates(): Boolean {
uiSafe(app, R.string.failed_to_check_updates, "Failed to check for updates") { uiSafe(app, R.string.failed_to_check_updates, "Failed to check for updates") {
app.toast(app.getString(R.string.update_check)) app.toast(app.getString(R.string.update_check))

View File

@ -180,6 +180,10 @@ fun LocalDateTime.relativeTime(context: Context): String {
private var transparentListItemColorsCached: ListItemColors? = null private var transparentListItemColorsCached: ListItemColors? = null
fun resetListItemColorsCached() {
transparentListItemColorsCached = null
}
/** /**
* The default ListItem colors, but with [ListItemColors.containerColor] set to [Color.Transparent]. * The default ListItem colors, but with [ListItemColors.containerColor] set to [Color.Transparent].
*/ */

View File

@ -38,6 +38,8 @@
<string name="android_11_bug_dialog_title">Android 11 bug</string> <string name="android_11_bug_dialog_title">Android 11 bug</string>
<string name="android_11_bug_dialog_description">The app installation permission must be granted ahead of time to avoid a bug in the Android 11 system that will negatively affect the user experience.</string> <string name="android_11_bug_dialog_description">The app installation permission must be granted ahead of time to avoid a bug in the Android 11 system that will negatively affect the user experience.</string>
<string name="no_network_toast">No internet connection available</string>
<string name="selected_app_meta_any_version">Any available version</string> <string name="selected_app_meta_any_version">Any available version</string>
<string name="app_source_dialog_title">Select source</string> <string name="app_source_dialog_title">Select source</string>
<string name="app_source_dialog_option_auto">Auto</string> <string name="app_source_dialog_option_auto">Auto</string>
@ -51,6 +53,9 @@
<string name="patch_selector_item_description">%d patches selected</string> <string name="patch_selector_item_description">%d patches selected</string>
<string name="no_patches_selected">No patches selected</string> <string name="no_patches_selected">No patches selected</string>
<string name="network_unavailable_warning">Your device is not connected to the internet. Downloading will fail later.</string>
<string name="network_metered_warning">You are currently on a metered connection. Data charges from your service provider may apply.</string>
<string name="apk_source_selector_item">Change source</string> <string name="apk_source_selector_item">Change source</string>
<string name="apk_source_auto">Current: All downloaders</string> <string name="apk_source_auto">Current: All downloaders</string>
<string name="apk_source_downloader">Current: %s</string> <string name="apk_source_downloader">Current: %s</string>
@ -87,7 +92,7 @@
<string name="theme_description">Choose between light or dark theme</string> <string name="theme_description">Choose between light or dark theme</string>
<string name="safeguards">Safeguards</string> <string name="safeguards">Safeguards</string>
<string name="patch_compat_check">Disable version compatibility check</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_description">The check restricts patches to compatible app versions</string>
<string name="patch_compat_check_confirmation">Selecting incompatible patches can result in a broken app.\n\nDo you want to proceed anyways?</string> <string name="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">Require suggested app version</string>
<string name="suggested_version_safeguard_description">Enforce selection of the suggested app version</string> <string name="suggested_version_safeguard_description">Enforce selection of the suggested app version</string>
@ -138,6 +143,8 @@
<string name="downloader_plugin_trust_dialog_title">Trust plugin?</string> <string name="downloader_plugin_trust_dialog_title">Trust plugin?</string>
<string name="downloader_plugin_revoke_trust_dialog_title">Revoke trust?</string> <string name="downloader_plugin_revoke_trust_dialog_title">Revoke trust?</string>
<string name="downloader_plugin_trust_dialog_body">Package name: %1$s\nSignature (SHA-256): %2$s</string> <string name="downloader_plugin_trust_dialog_body">Package name: %1$s\nSignature (SHA-256): %2$s</string>
<string name="downloader_plugin_delete_apps_title">Delete selected apps</string>
<string name="downloader_plugin_delete_apps_description">Are you sure you want to delete the selected apps?</string>
<string name="downloader_settings_no_apps">No downloaded apps found</string> <string name="downloader_settings_no_apps">No downloaded apps found</string>
<string name="search_apps">Search apps…</string> <string name="search_apps">Search apps…</string>
@ -211,7 +218,7 @@
<string name="no_patched_apps_found">No patched apps found</string> <string name="no_patched_apps_found">No patched apps found</string>
<string name="tap_on_patches">Tap on the patches to get more information about them</string> <string name="tap_on_patches">Tap on the patches to get more information about them</string>
<string name="bundles_selected">%s selected</string> <string name="bundles_selected">%s selected</string>
<string name="unsupported_patches">Incompatible patches</string> <string name="incompatible_patches">Incompatible patches</string>
<string name="universal_patches">Universal patches</string> <string name="universal_patches">Universal patches</string>
<string name="patch_selection_reset_toast">Patch selection and options has been reset to recommended defaults</string> <string name="patch_selection_reset_toast">Patch selection and options has been reset to recommended defaults</string>
<string name="patch_options_reset_toast">Patch options have been reset</string> <string name="patch_options_reset_toast">Patch options have been reset</string>
@ -220,13 +227,12 @@
<string name="selection_warning_title">Stop using defaults?</string> <string name="selection_warning_title">Stop using defaults?</string>
<string name="selection_warning_description">It is recommended to use the default patch selection and options. Changing them may result in unexpected issues.\n\nYou need to turn on \"Allow changing patch selection\" in the advanced settings before toggling patches.</string> <string name="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="universal_patch_warning_description">Universal patches have a more generalized use and do not work as reliably as patches that target specific apps. You may encounter issues while using them.\n\nThis warning can be disabled in the advanced settings.</string>
<string name="supported">This version</string> <string name="this_version">This version</string>
<string name="universal">Any app</string> <string name="universal">Any app</string>
<string name="unsupported">Unsupported</string>
<string name="search_patches">Search patches</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="app_version_not_compatible">This patch is not compatible with the selected app version (%1$s).\n\nIt is only compatible with the following version(s): %2$s.</string>
<string name="continue_with_version">Continue with this version?</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> <string name="version_not_compatible">Not all patches are compatible with this version (%s). Do you want to continue anyway?</string>
<string name="download_application">Download application?</string> <string name="download_application">Download application?</string>
<string name="app_not_installed">The app you selected isn\'t installed. Do you want to download it?</string> <string name="app_not_installed">The app you selected isn\'t installed. Do you want to download it?</string>
<string name="failed_to_load_apk">Failed to load APK</string> <string name="failed_to_load_apk">Failed to load APK</string>
@ -297,6 +303,8 @@
<string name="patcher_step_write_patched">Write patched APK file</string> <string name="patcher_step_write_patched">Write patched APK file</string>
<string name="patcher_step_sign_apk">Sign patched APK file</string> <string name="patcher_step_sign_apk">Sign patched APK file</string>
<string name="patcher_notification_message">Patching in progress…</string> <string name="patcher_notification_message">Patching in progress…</string>
<string name="patcher_stop_confirm_title">Stop patcher</string>
<string name="patcher_stop_confirm_description">Are you sure you want to stop the patching process?</string>
<string name="execute_patches">Execute patches</string> <string name="execute_patches">Execute patches</string>
<string name="executing_patch">Execute %s</string> <string name="executing_patch">Execute %s</string>
<string name="failed_to_execute_patch">Failed to execute %s</string> <string name="failed_to_execute_patch">Failed to execute %s</string>
@ -332,9 +340,17 @@
<string name="bundle_view_patches">View patches</string> <string name="bundle_view_patches">View patches</string>
<string name="bundle_view_patches_any_version">Any version</string> <string name="bundle_view_patches_any_version">Any version</string>
<string name="bundle_view_patches_any_package">Any package</string> <string name="bundle_view_patches_any_package">Any package</string>
<string name="bundle_delete_single_dialog_title">Delete bundle</string>
<string name="bundle_delete_multiple_dialog_title">Delete bundles</string>
<string name="bundle_delete_single_dialog_description">Are you sure you want to delete the bundle \"%s\"?</string>
<string name="bundle_delete_multiple_dialog_description">Are you sure you want to delete the selected bundles?</string>
<string name="about_revanced_manager">About ReVanced Manager</string> <string name="about_revanced_manager">About ReVanced Manager</string>
<string name="revanced_manager_description">ReVanced Manager is an application designed to work with ReVanced Patcher, which allows for long-lasting patches to be created for Android apps. The patching system is designed to automatically work with new versions of apps with minimal maintenance.</string> <string name="revanced_manager_description">ReVanced Manager is an application designed to work with ReVanced Patcher, which allows for long-lasting patches to be created for Android apps. The patching system is designed to automatically work with new versions of apps with minimal maintenance.</string>
<string name="developer_options_taps">%d taps remaining</string>
<string name="developer_options_enabled">Developer options enabled</string>
<string name="developer_options_already_enabled">Developer options are already enabled</string>
<string name="update_available">An update is available</string> <string name="update_available">An update is available</string>
<string name="current_version">Current version: %s</string> <string name="current_version">Current version: %s</string>
<string name="new_version">New version: %s</string> <string name="new_version">New version: %s</string>
@ -400,7 +416,7 @@
<string name="installation_aborted_description">The installation was cancelled manually. Try again?</string> <string name="installation_aborted_description">The installation was cancelled manually. Try again?</string>
<string name="installation_blocked_description">The installation was blocked. Review your device security settings and try again.</string> <string name="installation_blocked_description">The installation was blocked. Review your device security settings and try again.</string>
<string name="installation_conflict_description">The installation was prevented by an existing installation of the app. Uninstall the installed app and try again?</string> <string name="installation_conflict_description">The installation was prevented by an existing installation of the app. Uninstall the installed app and try again?</string>
<string name="installation_incompatible_description">The app is incompatible with this device. Use an APK that is supported by this device and try again.</string> <string name="installation_incompatible_description">The app is incompatible with this device. Use an APK that is compatible by this device and try again.</string>
<string name="installation_invalid_description">The app is invalid. Uninstall the app and try again?</string> <string name="installation_invalid_description">The app is invalid. Uninstall the app and try again?</string>
<string name="installation_storage_issue_description">The app could not be installed due to insufficient storage. Free up some space and try again.</string> <string name="installation_storage_issue_description">The app could not be installed due to insufficient storage. Free up some space and try again.</string>
<string name="installation_timeout_description">The installation took too long. Try again?</string> <string name="installation_timeout_description">The installation took too long. Try again?</string>
@ -413,10 +429,13 @@
<string name="add_patch_bundle">Add patch bundle</string> <string name="add_patch_bundle">Add patch bundle</string>
<string name="bundle_url">Bundle URL</string> <string name="bundle_url">Bundle URL</string>
<string name="auto_update">Auto update</string> <string name="auto_update">Auto update</string>
<string name="unsupported_patches_dialog">These patches are not compatible with the selected app version (%1$s).\n\nClick on the patches to see more details.</string> <string name="incompatible_patches_dialog">These patches are not compatible with the selected app version (%1$s).\n\nClick on the patches to see more details.</string>
<string name="unsupported_patch">Unsupported patch</string> <string name="incompatible_patch">Incompatible patch</string>
<string name="any_version">Any</string> <string name="any_version">Any</string>
<string name="never_show_again">Never show again</string> <string name="never_show_again">Never show again</string>
<string name="show_manager_update_dialog_on_launch">Show update message on launch</string> <string name="show_manager_update_dialog_on_launch">Show update message on launch</string>
<string name="show_manager_update_dialog_on_launch_description">Shows a popup notification whenever there is a new update available on launch.</string> <string name="show_manager_update_dialog_on_launch_description">Shows a popup notification whenever there is a new update available on launch.</string>
<string name="failed_to_import_keystore">Failed to import keystore</string>
<string name="export">Export</string>
<string name="confirm">Confirm</string>
</resources> </resources>

View File

@ -7,10 +7,4 @@ plugins {
alias(libs.plugins.kotlin.parcelize) apply false alias(libs.plugins.kotlin.parcelize) apply false
alias(libs.plugins.about.libraries) apply false alias(libs.plugins.about.libraries) apply false
alias(libs.plugins.compose.compiler) apply false alias(libs.plugins.compose.compiler) apply false
alias(libs.plugins.binary.compatibility.validator)
} }
apiValidation {
ignoredProjects.addAll(listOf("app", "example-downloader-plugin"))
nonPublicMarkers += "app.revanced.manager.plugin.downloader.PluginHostApi"
}

View File

@ -1,11 +1,10 @@
# 💼 Prerequisites # 💼 Prerequisites
To use ReVanced Manager, certain requirements have to be met. In order to use ReVanced Manager, certain requirements must be met.
## 🤝 Requirements ## 🤝 Requirements
- Android device running Android 8.0 or higher - An Android device running Android 8 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 ## ⏭️ What's next

View File

@ -1,26 +1,14 @@
# ⬇️ Installation # ⬇️ Installation
To use ReVanced on your Android device, ReVanced Manager have to be install, In order to use ReVanced on your Android device, ReVanced Manager must be installed.
refer to the [Prerequisites](0_prerequisites.md) if haven't already.
## 🪜 Installation steps ## Installation steps
1. Get the latest version of ReVanced Manager from the [ReVanced site][Official ReVanced Download]. 1. Download the latest version of ReVanced Manager from [here](https://github.com/revanced/revanced-manager/releases/latest)
2. Install ReVanced Manager 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 ## ⏭️ What's next
The next page will guide you through using ReVanced Manager. The next page will guide you through using ReVanced Manager.
Continue: [🛠️ Usage](2_usage.md) Continue: [🛠️ Usage](2_usage.md)
[Official ReVanced Download]: https://revanced.app/download

View File

@ -8,26 +8,16 @@ The following pages will guide you through using ReVanced Manager to patch apps.
2. Tap the + button in the bottom right corner 2. Tap the + button in the bottom right corner
3. Choose an app to patch[^1] 3. Choose an app to patch[^1]
4. Tap on the version of the app you want to patch[^2] 4. Tap on the version of the app you want to patch[^2]
5. Tap the 🪄 **Patch** button 5. Select the patches you want to apply
6. Tap on the 📲 **Install** button 6. Tap the Patch button
> [!Note] 7. Tap on the **Install** button
> If the **Install** Button isn't there, it could be that something went wrong. Refer to [3. ❔ Troubleshooting](3_troubleshooting.md) > **Note**: If you are rooted, you can mount the patched app on top of the original app.[^3]
> [!Tip] > Optionally, you may export the patched app to storage using the options in the top right corner.
> 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]: 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. [^1]: Non-root users may be prompted to select an APK from storage, in which case you have to source the APK file yourself. ReVanced does not provide any APK files.
[^2]: It is suggested to use the version with the most patches to get the most out of ReVanced. [^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. [^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 ## ⏭️ What's next
The next page will bring you back to the usage page. The next page will bring you back to the usage page.

View File

@ -6,13 +6,10 @@ 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 1. Navigate to the Apps tab from the top navigation bar
2. Select the app you want to manage 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 ## ⏭️ What's next
The next page will bring you back to the usage page. The next page will bring you back to the usage page.
Continue: [🛠️ Usage](2_usage.md) Continue: [🛠️ Usage](2_usage.md)

View File

@ -4,25 +4,7 @@ In order to keep up with the latest features and bug fixes, it is recommended to
## ✅ Updating steps ## ✅ Updating steps
1. Tap the ↻ Update button in the top right corner right next to the ⚙️ Settings icon > Currently not implemented
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 ## ⏭️ What's next

View File

@ -8,44 +8,30 @@ 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. 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 - ### 🧬 Sources
Override the API and change the source of ReVanced Patches. Override the API and change the source of ReVanced Patches.
Location: `Patch bundles -> + Button` - ### 🧪 Experimental ReVanced Patches support
- ### 🧪 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. 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 - ### 🧑‍🔬 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. 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 - ### 🔑 Export, import or delete keystore
Manage the keystore used to sign patched apps. Manage the keystore used to sign patched apps.
Location: `⚙️ Settings -> Import & export -> Signing` - ### 📄 Export, import or reset ReVanced Patches selection
- ### 📄 Export, import or reset patch selection Manage the ReVanced Patches selection. This is useful if you want to share your ReVanced Patches selection with others or reset it to the default selection.
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 - ### About
View information about your device and ReVanced Manager. This includes the version of ReVanced Manager and supported architectures of your device. 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 ## ⏭️ What's next
The next page will bring you back to the usage page. The next page will bring you back to the usage page.

View File

@ -10,19 +10,11 @@ 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. 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 - ❗️ 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. Your device is not supported. Refer to the [Prerequisites](0_prerequisites.md) page for supported devices.
Alternatively, you can use [ReVanced CLI][ReVanced CLI GitHub]) to patch the app. Alternatively, you can use [ReVanced CLI](https://github.com/revanced/revanced-cli) to patch the app.
[ReVanced CLI GitHub]: https://github.com/revanced/revanced-cli
- 🚫 Non-root install is not possible with the current patches selection - 🚫 Non-root install is not possible with the current patches selection
@ -30,14 +22,10 @@ In case you encounter any issues while using ReVanced Manager, please refer to t
- 🚨 Patched app crashes on launch - 🚨 Patched app crashes on launch
Try selecting the **Default** button when choosing patches, if the patched app still failed, file an issue at [ReVanced Patches][ReVanced Patches GitHub]. Select the **Default** button when choosing patches.
[ReVanced Patches GitHub]: https://github.com/revanced/revanced-patches
## ⏭️ What's next ## ⏭️ What's next
You have successfully finished the guide for on how to use ReVanced Manager. The next page will teach you how to build ReVanced Manager from source.
If you wish to review the Table of Content for how to use ReVanced Manager, feel free to do so. Continue: [🔨 Building from source](4_building.md)
Continue: [💊 ReVanced Manager](/docs/README.md)

38
docs/4_building.md Normal file
View File

@ -0,0 +1,38 @@
# 🛠️ 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 # 💊 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 ## 📖 Table of contents
@ -12,10 +12,7 @@ This documentation explains how to use [ReVanced Manager](https://github.com/ReV
3. [🔄 Updating ReVanced Manager](2_3_updating.md) 3. [🔄 Updating ReVanced Manager](2_3_updating.md)
4. [⚙️ Configuring ReVanced Manager](2_4_settings.md) 4. [⚙️ Configuring ReVanced Manager](2_4_settings.md)
3. [❔ Troubleshooting](3_troubleshooting.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 ## ⏭️ Start here

View File

@ -1,28 +0,0 @@
# 💼 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

@ -1,25 +0,0 @@
# 🧑‍💻 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

@ -1,21 +0,0 @@
# 🎉 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

@ -1,19 +0,0 @@
# ✒️ 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/

View File

@ -1,25 +0,0 @@
# 💊 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

@ -1 +0,0 @@
/build

View File

@ -1,171 +0,0 @@
public abstract interface class app/revanced/manager/plugin/downloader/BaseDownloadScope : app/revanced/manager/plugin/downloader/Scope {
}
public final class app/revanced/manager/plugin/downloader/ConstantsKt {
public static final field PLUGIN_HOST_PERMISSION Ljava/lang/String;
}
public final class app/revanced/manager/plugin/downloader/DownloadUrl : android/os/Parcelable {
public static final field CREATOR Landroid/os/Parcelable$Creator;
public fun <init> (Ljava/lang/String;Ljava/util/Map;)V
public synthetic fun <init> (Ljava/lang/String;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Ljava/lang/String;
public final fun component2 ()Ljava/util/Map;
public final fun copy (Ljava/lang/String;Ljava/util/Map;)Lapp/revanced/manager/plugin/downloader/DownloadUrl;
public static synthetic fun copy$default (Lapp/revanced/manager/plugin/downloader/DownloadUrl;Ljava/lang/String;Ljava/util/Map;ILjava/lang/Object;)Lapp/revanced/manager/plugin/downloader/DownloadUrl;
public final fun describeContents ()I
public fun equals (Ljava/lang/Object;)Z
public final fun getHeaders ()Ljava/util/Map;
public final fun getUrl ()Ljava/lang/String;
public fun hashCode ()I
public final fun toDownloadResult ()Lkotlin/Pair;
public fun toString ()Ljava/lang/String;
public final fun writeToParcel (Landroid/os/Parcel;I)V
}
public final class app/revanced/manager/plugin/downloader/DownloadUrl$Creator : android/os/Parcelable$Creator {
public fun <init> ()V
public final fun createFromParcel (Landroid/os/Parcel;)Lapp/revanced/manager/plugin/downloader/DownloadUrl;
public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object;
public final fun newArray (I)[Lapp/revanced/manager/plugin/downloader/DownloadUrl;
public synthetic fun newArray (I)[Ljava/lang/Object;
}
public final class app/revanced/manager/plugin/downloader/Downloader {
}
public final class app/revanced/manager/plugin/downloader/DownloaderBuilder {
}
public final class app/revanced/manager/plugin/downloader/DownloaderKt {
public static final fun Downloader (Lkotlin/jvm/functions/Function1;)Lapp/revanced/manager/plugin/downloader/DownloaderBuilder;
}
public final class app/revanced/manager/plugin/downloader/DownloaderScope : app/revanced/manager/plugin/downloader/Scope {
public final fun download (Lkotlin/jvm/functions/Function3;)V
public final fun get (Lkotlin/jvm/functions/Function4;)V
public fun getHostPackageName ()Ljava/lang/String;
public fun getPluginPackageName ()Ljava/lang/String;
public final fun useService (Landroid/content/Intent;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}
public final class app/revanced/manager/plugin/downloader/ExtensionsKt {
public static final fun download (Lapp/revanced/manager/plugin/downloader/DownloaderScope;Lkotlin/jvm/functions/Function4;)V
}
public abstract interface class app/revanced/manager/plugin/downloader/GetScope : app/revanced/manager/plugin/downloader/Scope {
public abstract fun requestStartActivity (Landroid/content/Intent;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}
public abstract interface class app/revanced/manager/plugin/downloader/InputDownloadScope : app/revanced/manager/plugin/downloader/BaseDownloadScope {
}
public abstract interface class app/revanced/manager/plugin/downloader/OutputDownloadScope : app/revanced/manager/plugin/downloader/BaseDownloadScope {
public abstract fun reportSize (JLkotlin/coroutines/Continuation;)Ljava/lang/Object;
}
public final class app/revanced/manager/plugin/downloader/Package : android/os/Parcelable {
public static final field CREATOR Landroid/os/Parcelable$Creator;
public fun <init> (Ljava/lang/String;Ljava/lang/String;)V
public final fun component1 ()Ljava/lang/String;
public final fun component2 ()Ljava/lang/String;
public final fun copy (Ljava/lang/String;Ljava/lang/String;)Lapp/revanced/manager/plugin/downloader/Package;
public static synthetic fun copy$default (Lapp/revanced/manager/plugin/downloader/Package;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lapp/revanced/manager/plugin/downloader/Package;
public final fun describeContents ()I
public fun equals (Ljava/lang/Object;)Z
public final fun getName ()Ljava/lang/String;
public final fun getVersion ()Ljava/lang/String;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
public final fun writeToParcel (Landroid/os/Parcel;I)V
}
public final class app/revanced/manager/plugin/downloader/Package$Creator : android/os/Parcelable$Creator {
public fun <init> ()V
public final fun createFromParcel (Landroid/os/Parcel;)Lapp/revanced/manager/plugin/downloader/Package;
public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object;
public final fun newArray (I)[Lapp/revanced/manager/plugin/downloader/Package;
public synthetic fun newArray (I)[Ljava/lang/Object;
}
public abstract interface annotation class app/revanced/manager/plugin/downloader/PluginHostApi : java/lang/annotation/Annotation {
}
public abstract interface class app/revanced/manager/plugin/downloader/Scope {
public abstract fun getHostPackageName ()Ljava/lang/String;
public abstract fun getPluginPackageName ()Ljava/lang/String;
}
public abstract class app/revanced/manager/plugin/downloader/UserInteractionException : java/lang/Exception {
public synthetic fun <init> (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
}
public abstract class app/revanced/manager/plugin/downloader/UserInteractionException$Activity : app/revanced/manager/plugin/downloader/UserInteractionException {
public synthetic fun <init> (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
}
public final class app/revanced/manager/plugin/downloader/UserInteractionException$Activity$Cancelled : app/revanced/manager/plugin/downloader/UserInteractionException$Activity {
}
public final class app/revanced/manager/plugin/downloader/UserInteractionException$Activity$NotCompleted : app/revanced/manager/plugin/downloader/UserInteractionException$Activity {
public final fun getIntent ()Landroid/content/Intent;
public final fun getResultCode ()I
}
public final class app/revanced/manager/plugin/downloader/UserInteractionException$RequestDenied : app/revanced/manager/plugin/downloader/UserInteractionException {
}
public final class app/revanced/manager/plugin/downloader/webview/APIKt {
public static final fun WebViewDownloader (Lkotlin/jvm/functions/Function4;)Lapp/revanced/manager/plugin/downloader/DownloaderBuilder;
public static final fun runWebView (Lapp/revanced/manager/plugin/downloader/GetScope;Ljava/lang/String;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}
public class app/revanced/manager/plugin/downloader/webview/IWebView$Default : app/revanced/manager/plugin/downloader/webview/IWebView {
public fun <init> ()V
public fun asBinder ()Landroid/os/IBinder;
public fun finish ()V
public fun load (Ljava/lang/String;)V
}
public abstract class app/revanced/manager/plugin/downloader/webview/IWebView$Stub : android/os/Binder, app/revanced/manager/plugin/downloader/webview/IWebView {
public fun <init> ()V
public fun asBinder ()Landroid/os/IBinder;
public static fun asInterface (Landroid/os/IBinder;)Lapp/revanced/manager/plugin/downloader/webview/IWebView;
public fun onTransact (ILandroid/os/Parcel;Landroid/os/Parcel;I)Z
}
public class app/revanced/manager/plugin/downloader/webview/IWebViewEvents$Default : app/revanced/manager/plugin/downloader/webview/IWebViewEvents {
public fun <init> ()V
public fun asBinder ()Landroid/os/IBinder;
public fun download (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
public fun pageLoad (Ljava/lang/String;)V
public fun ready (Lapp/revanced/manager/plugin/downloader/webview/IWebView;)V
}
public abstract class app/revanced/manager/plugin/downloader/webview/IWebViewEvents$Stub : android/os/Binder, app/revanced/manager/plugin/downloader/webview/IWebViewEvents {
public fun <init> ()V
public fun asBinder ()Landroid/os/IBinder;
public static fun asInterface (Landroid/os/IBinder;)Lapp/revanced/manager/plugin/downloader/webview/IWebViewEvents;
public fun onTransact (ILandroid/os/Parcel;Landroid/os/Parcel;I)Z
}
public final class app/revanced/manager/plugin/downloader/webview/WebViewActivity$Parameters$Creator : android/os/Parcelable$Creator {
public fun <init> ()V
public final fun createFromParcel (Landroid/os/Parcel;)Lapp/revanced/manager/plugin/downloader/webview/WebViewActivity$Parameters;
public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object;
public final fun newArray (I)[Lapp/revanced/manager/plugin/downloader/webview/WebViewActivity$Parameters;
public synthetic fun newArray (I)[Ljava/lang/Object;
}
public abstract interface class app/revanced/manager/plugin/downloader/webview/WebViewCallbackScope : app/revanced/manager/plugin/downloader/Scope {
public abstract fun finish (Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun load (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}
public final class app/revanced/manager/plugin/downloader/webview/WebViewScope : app/revanced/manager/plugin/downloader/Scope {
public final fun download (Lkotlin/jvm/functions/Function5;)V
public fun getHostPackageName ()Ljava/lang/String;
public fun getPluginPackageName ()Ljava/lang/String;
public final fun pageLoad (Lkotlin/jvm/functions/Function3;)V
}

View File

@ -1,61 +0,0 @@
plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.parcelize)
`maven-publish`
}
android {
namespace = "app.revanced.manager.plugin.downloader"
compileSdk = 35
defaultConfig {
minSdk = 26
consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
buildFeatures {
aidl = true
}
}
dependencies {
implementation(libs.androidx.ktx)
implementation(libs.activity.ktx)
implementation(libs.runtime.ktx)
implementation(libs.appcompat)
}
publishing {
repositories {
mavenLocal()
}
publications {
create<MavenPublication>("release") {
groupId = "app.revanced"
artifactId = "manager-downloader-plugin"
version = "1.0"
afterEvaluate {
from(components["release"])
}
}
}
}

View File

@ -1,21 +0,0 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@ -1,3 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>

View File

@ -1,8 +0,0 @@
// IWebView.aidl
package app.revanced.manager.plugin.downloader.webview;
@JavaPassthrough(annotation="@app.revanced.manager.plugin.downloader.PluginHostApi")
oneway interface IWebView {
void load(String url);
void finish();
}

View File

@ -1,11 +0,0 @@
// IWebViewEvents.aidl
package app.revanced.manager.plugin.downloader.webview;
import app.revanced.manager.plugin.downloader.webview.IWebView;
@JavaPassthrough(annotation="@app.revanced.manager.plugin.downloader.PluginHostApi")
oneway interface IWebViewEvents {
void ready(IWebView iface);
void pageLoad(String url);
void download(String url, String mimetype, String userAgent);
}

View File

@ -1,7 +0,0 @@
package app.revanced.manager.plugin.downloader
/**
* The permission ID of the special plugin host permission. Only ReVanced Manager will have this permission.
* Plugin UI activities and internal services can be protected using this permission.
*/
const val PLUGIN_HOST_PERMISSION = "app.revanced.manager.permission.PLUGIN_HOST"

View File

@ -1,165 +0,0 @@
package app.revanced.manager.plugin.downloader
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.IBinder
import android.app.Activity
import android.os.Parcelable
import kotlinx.coroutines.withTimeout
import java.io.InputStream
import java.io.OutputStream
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
@RequiresOptIn(
level = RequiresOptIn.Level.ERROR,
message = "This API is only intended for plugin hosts, don't use it in a plugin.",
)
@Retention(AnnotationRetention.BINARY)
annotation class PluginHostApi
/**
* The base interface for all DSL scopes.
*/
interface Scope {
/**
* The package name of ReVanced Manager.
*/
val hostPackageName: String
/**
* The package name of the plugin.
*/
val pluginPackageName: String
}
/**
* The scope of [DownloaderScope.get].
*/
interface GetScope : Scope {
/**
* Ask the user to perform some required interaction in the activity specified by the provided [Intent].
* This function returns normally with the resulting [Intent] when the activity finishes with code [Activity.RESULT_OK].
*
* @throws UserInteractionException.RequestDenied User decided to skip this plugin.
* @throws UserInteractionException.Activity.Cancelled The activity was cancelled.
* @throws UserInteractionException.Activity.NotCompleted The activity finished with an unknown result code.
*/
suspend fun requestStartActivity(intent: Intent): Intent?
}
interface BaseDownloadScope : Scope
/**
* The scope for [DownloaderScope.download].
*/
interface InputDownloadScope : BaseDownloadScope
typealias Size = Long
typealias DownloadResult = Pair<InputStream, Size?>
typealias Version = String
typealias GetResult<T> = Pair<T, Version?>
class DownloaderScope<T : Parcelable> internal constructor(
private val scopeImpl: Scope,
internal val context: Context
) : Scope by scopeImpl {
// Returning an InputStream is the primary way for plugins to implement the download function, but we also want to offer an OutputStream API since using InputStream might not be convenient in all cases.
// It is much easier to implement the main InputStream API on top of OutputStreams compared to doing it the other way around, which is why we are using OutputStream here. This detail is not visible to plugins.
internal var download: (suspend OutputDownloadScope.(T, OutputStream) -> Unit)? = null
internal var get: (suspend GetScope.(String, String?) -> GetResult<T>?)? = null
private val inputDownloadScopeImpl = object : InputDownloadScope, Scope by scopeImpl {}
/**
* Define the download block of the plugin.
*/
fun download(block: suspend InputDownloadScope.(data: T) -> DownloadResult) {
download = { app, outputStream ->
val (inputStream, size) = inputDownloadScopeImpl.block(app)
inputStream.use {
if (size != null) reportSize(size)
it.copyTo(outputStream)
}
}
}
/**
* Define the get block of the plugin.
* The block should return null if the app cannot be found. The version in the result must match the version argument unless it is null.
*/
fun get(block: suspend GetScope.(packageName: String, version: String?) -> GetResult<T>?) {
get = block
}
/**
* Utilize the service specified by the provided [Intent]. The service will be unbound when the scope ends.
*/
suspend fun <R : Any?> useService(intent: Intent, block: suspend (IBinder) -> R): R {
var onBind: ((IBinder) -> Unit)? = null
val serviceConn = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) =
onBind!!(service!!)
override fun onServiceDisconnected(name: ComponentName?) {}
}
return try {
val binder = withTimeout(10000L) {
suspendCoroutine { continuation ->
onBind = continuation::resume
context.bindService(intent, serviceConn, Context.BIND_AUTO_CREATE)
}
}
block(binder)
} finally {
onBind = null
context.unbindService(serviceConn)
}
}
}
class DownloaderBuilder<T : Parcelable> internal constructor(private val block: DownloaderScope<T>.() -> Unit) {
@PluginHostApi
fun build(scopeImpl: Scope, context: Context) =
with(DownloaderScope<T>(scopeImpl, context)) {
block()
Downloader(
download = download!!,
get = get!!
)
}
}
class Downloader<T : Parcelable> internal constructor(
@property:PluginHostApi val get: suspend GetScope.(packageName: String, version: String?) -> GetResult<T>?,
@property:PluginHostApi val download: suspend OutputDownloadScope.(data: T, outputStream: OutputStream) -> Unit
)
/**
* Define a downloader plugin.
*/
fun <T : Parcelable> Downloader(block: DownloaderScope<T>.() -> Unit) = DownloaderBuilder(block)
/**
* @see GetScope.requestStartActivity
*/
sealed class UserInteractionException(message: String) : Exception(message) {
class RequestDenied @PluginHostApi constructor() :
UserInteractionException("Request denied by user")
sealed class Activity(message: String) : UserInteractionException(message) {
class Cancelled @PluginHostApi constructor() : Activity("Interaction cancelled")
/**
* @param resultCode The result code of the activity.
* @param intent The [Intent] of the activity.
*/
class NotCompleted @PluginHostApi constructor(val resultCode: Int, val intent: Intent?) :
Activity("Unexpected activity result code: $resultCode")
}
}

View File

@ -1,42 +0,0 @@
package app.revanced.manager.plugin.downloader
import android.app.Activity
import android.app.Service
import android.content.Intent
import android.os.IBinder
import android.os.Parcelable
import java.io.OutputStream
/**
* The scope of the [OutputStream] version of [DownloaderScope.download].
*/
interface OutputDownloadScope : BaseDownloadScope {
suspend fun reportSize(size: Long)
}
/**
* A replacement for [DownloaderScope.download] that uses [OutputStream].
* The provided [OutputStream] does not need to be closed manually.
*/
fun <T : Parcelable> DownloaderScope<T>.download(block: suspend OutputDownloadScope.(T, OutputStream) -> Unit) {
download = block
}
/**
* Performs [GetScope.requestStartActivity] with an [Intent] created using the type information of [ACTIVITY].
* @see [GetScope.requestStartActivity]
*/
suspend inline fun <reified ACTIVITY : Activity> GetScope.requestStartActivity() =
requestStartActivity(
Intent().apply { setClassName(pluginPackageName, ACTIVITY::class.qualifiedName!!) }
)
/**
* Performs [DownloaderScope.useService] with an [Intent] created using the type information of [SERVICE].
* @see [DownloaderScope.useService]
*/
suspend inline fun <reified SERVICE : Service, R : Any?> DownloaderScope<*>.useService(
noinline block: suspend (IBinder) -> R
) = useService(
Intent().apply { setClassName(pluginPackageName, SERVICE::class.qualifiedName!!) }, block
)

View File

@ -1,39 +0,0 @@
package app.revanced.manager.plugin.downloader
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import java.net.HttpURLConnection
import java.net.URI
/**
* A simple parcelable data class for storing a package name and version.
* This can be used as the data type for plugins that only need a name and version to implement their [DownloaderScope.download] function.
*
* @param name The package name.
* @param version The version.
*/
@Parcelize
data class Package(val name: String, val version: String) : Parcelable
/**
* A data class for storing a download URL.
*
* @param url The download URL.
* @param headers The headers to use for the request.
*/
@Parcelize
data class DownloadUrl(val url: String, val headers: Map<String, String> = emptyMap()) : Parcelable {
/**
* Converts this into a [DownloadResult].
*/
fun toDownloadResult(): DownloadResult = with(URI.create(url).toURL().openConnection() as HttpURLConnection) {
useCaches = false
allowUserInteraction = false
headers.forEach(::setRequestProperty)
connectTimeout = 10_000
connect()
inputStream to getHeaderField("Content-Length").toLong()
}
}

View File

@ -1,176 +0,0 @@
package app.revanced.manager.plugin.downloader.webview
import android.content.Intent
import app.revanced.manager.plugin.downloader.DownloadUrl
import app.revanced.manager.plugin.downloader.DownloaderScope
import app.revanced.manager.plugin.downloader.GetScope
import app.revanced.manager.plugin.downloader.Scope
import app.revanced.manager.plugin.downloader.Downloader
import app.revanced.manager.plugin.downloader.PluginHostApi
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.launch
import kotlinx.coroutines.supervisorScope
import kotlinx.coroutines.withContext
import kotlin.properties.Delegates
typealias InitialUrl = String
typealias PageLoadCallback<T> = suspend WebViewCallbackScope<T>.(url: String) -> Unit
typealias DownloadCallback<T> = suspend WebViewCallbackScope<T>.(url: String, mimeType: String, userAgent: String) -> Unit
interface WebViewCallbackScope<T> : Scope {
/**
* Finishes the activity and returns the [result].
*/
suspend fun finish(result: T)
/**
* Tells the WebView to load the specified [url].
*/
suspend fun load(url: String)
}
@OptIn(PluginHostApi::class)
class WebViewScope<T> internal constructor(
coroutineScope: CoroutineScope,
private val scopeImpl: Scope,
setResult: (T) -> Unit
) : Scope by scopeImpl {
private var onPageLoadCallback: PageLoadCallback<T> = {}
private var onDownloadCallback: DownloadCallback<T> = { _, _, _ -> }
@OptIn(ExperimentalCoroutinesApi::class)
private val dispatcher = Dispatchers.Default.limitedParallelism(1)
private lateinit var webView: IWebView
internal lateinit var initialUrl: String
internal val binder = object : IWebViewEvents.Stub() {
override fun ready(iface: IWebView?) {
coroutineScope.launch(dispatcher) {
webView = iface!!.also {
it.load(initialUrl)
}
}
}
override fun pageLoad(url: String?) {
coroutineScope.launch(dispatcher) { onPageLoadCallback(callbackScope, url!!) }
}
override fun download(url: String?, mimetype: String?, userAgent: String?) {
coroutineScope.launch(dispatcher) {
onDownloadCallback(
callbackScope,
url!!,
mimetype!!,
userAgent!!
)
}
}
}
private val callbackScope = object : WebViewCallbackScope<T>, Scope by scopeImpl {
override suspend fun finish(result: T) {
setResult(result)
// Tell the WebViewActivity to finish
webView.let { withContext(Dispatchers.IO) { it.finish() } }
}
override suspend fun load(url: String) {
webView.let { withContext(Dispatchers.IO) { it.load(url) } }
}
}
/**
* Called when the WebView attempts to download a file to disk.
*/
fun download(block: DownloadCallback<T>) {
onDownloadCallback = block
}
/**
* Called when the WebView finishes loading a page.
*/
fun pageLoad(block: PageLoadCallback<T>) {
onPageLoadCallback = block
}
}
@JvmInline
private value class Container<U>(val value: U)
/**
* Run a [android.webkit.WebView] Activity controlled by the provided code block.
* The activity will keep running until it is cancelled or an event handler calls [WebViewCallbackScope.finish].
* The [block] defines the event handlers and returns the initial URL.
*
* @param title The string displayed in the action bar.
* @param block The control block.
*/
@OptIn(PluginHostApi::class)
suspend fun <T> GetScope.runWebView(
title: String,
block: suspend WebViewScope<T>.() -> InitialUrl
) = supervisorScope {
var result by Delegates.notNull<Container<T>>()
val scope = WebViewScope<T>(this@supervisorScope, this@runWebView) { result = Container(it) }
scope.initialUrl = scope.block()
// Start the webview activity and wait until it finishes.
requestStartActivity(Intent().apply {
putExtra(
WebViewActivity.KEY,
WebViewActivity.Parameters(title, scope.binder)
)
setClassName(
hostPackageName,
WebViewActivity::class.qualifiedName!!
)
})
// Return the result and cancel any leftover coroutines.
coroutineContext.cancelChildren()
result.value
}
/**
* Implement a downloader using [runWebView] and [DownloadUrl]. This function will automatically define a handler for download events unlike [runWebView].
* Returning null inside the [block] is equivalent to returning null inside [DownloaderScope.get].
*
* @see runWebView
*/
fun WebViewDownloader(block: suspend WebViewScope<DownloadUrl>.(packageName: String, version: String?) -> InitialUrl?) =
Downloader<DownloadUrl> {
val label = context.applicationInfo.loadLabel(
context.packageManager
).toString()
get { packageName, version ->
class ReturnNull : Exception()
try {
runWebView(label) {
download { url, _, userAgent ->
finish(
DownloadUrl(
url,
mapOf("User-Agent" to userAgent)
)
)
}
block(this@runWebView, packageName, version) ?: throw ReturnNull()
} to version
} catch (_: ReturnNull) {
null
}
}
download {
it.toDownloadResult()
}
}

View File

@ -1,161 +0,0 @@
package app.revanced.manager.plugin.downloader.webview
import android.annotation.SuppressLint
import android.os.Bundle
import android.os.IBinder
import android.os.Parcelable
import android.view.MenuItem
import android.webkit.CookieManager
import android.webkit.WebSettings
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.activity.ComponentActivity
import androidx.activity.addCallback
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.lifecycle.viewModelScope
import app.revanced.manager.plugin.downloader.PluginHostApi
import app.revanced.manager.plugin.downloader.R
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
@OptIn(PluginHostApi::class)
@PluginHostApi
class WebViewActivity : ComponentActivity() {
@SuppressLint("SetJavaScriptEnabled")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val vm by viewModels<WebViewModel>()
enableEdgeToEdge()
setContentView(R.layout.activity_webview)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
val webView = findViewById<WebView>(R.id.webview)
onBackPressedDispatcher.addCallback {
if (webView.canGoBack()) webView.goBack()
else cancelActivity()
}
val params = intent.getParcelableExtra<Parameters>(KEY)!!
actionBar?.apply {
title = params.title
setHomeAsUpIndicator(android.R.drawable.ic_menu_close_clear_cancel)
setDisplayHomeAsUpEnabled(true)
}
val events = IWebViewEvents.Stub.asInterface(params.events)!!
vm.setup(events)
webView.apply {
settings.apply {
cacheMode = WebSettings.LOAD_NO_CACHE
allowContentAccess = false
domStorageEnabled = true
javaScriptEnabled = true
}
webViewClient = vm.webViewClient
setDownloadListener { url, userAgent, _, mimetype, _ ->
vm.onDownload(url, mimetype, userAgent)
}
}
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
vm.commands.collect {
when (it) {
is WebViewModel.Command.Finish -> {
setResult(RESULT_OK)
finish()
}
is WebViewModel.Command.Load -> webView.loadUrl(it.url)
}
}
}
}
}
private fun cancelActivity() {
setResult(RESULT_CANCELED)
finish()
}
override fun onOptionsItemSelected(item: MenuItem) = if (item.itemId == android.R.id.home) {
cancelActivity()
true
} else super.onOptionsItemSelected(item)
@Parcelize
internal class Parameters(
val title: String, val events: IBinder
) : Parcelable
internal companion object {
const val KEY = "params"
}
}
@OptIn(PluginHostApi::class)
internal class WebViewModel : ViewModel() {
init {
CookieManager.getInstance().apply {
removeAllCookies(null)
setAcceptCookie(true)
}
}
private val commandChannel = Channel<Command>()
val commands = commandChannel.receiveAsFlow()
private var eventBinder: IWebViewEvents? = null
private val ctrlBinder = object : IWebView.Stub() {
override fun load(url: String?) {
viewModelScope.launch {
commandChannel.send(Command.Load(url!!))
}
}
override fun finish() {
viewModelScope.launch {
commandChannel.send(Command.Finish)
}
}
}
val webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
eventBinder!!.pageLoad(url)
}
}
fun onDownload(url: String, mimeType: String, userAgent: String) {
eventBinder!!.download(url, mimeType, userAgent)
}
fun setup(binder: IWebViewEvents) {
if (eventBinder != null) return
eventBinder = binder
binder.ready(ctrlBinder)
}
sealed interface Command {
data class Load(val url: String) : Command
data object Finish : Command
}
}

View File

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/main">
<WebView
android:id="@+id/webview"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>

View File

@ -1 +0,0 @@
<resources></resources>

View File

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.WebViewActivity" parent="Theme.AppCompat.DayNight">
<item name="android:windowActionBar">true</item>
<item name="android:windowNoTitle">false</item>
</style>
</resources>

View File

@ -1 +0,0 @@
/build

View File

@ -1,53 +0,0 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.parcelize)
alias(libs.plugins.compose.compiler)
}
android {
val packageName = "app.revanced.manager.plugin.downloader.example"
namespace = packageName
compileSdk = 35
defaultConfig {
applicationId = packageName
minSdk = 26
targetSdk = 35
versionCode = 1
versionName = "1.0"
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
if (project.hasProperty("signAsDebug")) {
signingConfig = signingConfigs.getByName("debug")
}
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
buildFeatures.compose = true
}
dependencies {
implementation(libs.activity.compose)
implementation(platform(libs.compose.bom))
implementation(libs.compose.ui)
implementation(libs.compose.ui.tooling)
implementation(libs.compose.material3)
compileOnly(project(":downloader-plugin"))
}

View File

@ -1,21 +0,0 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@ -1,23 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-feature android:name="app.revanced.manager.plugin.downloader" />
<application
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
tools:targetApi="34">
<activity
android:name=".InteractionActivity"
android:exported="true"
android:permission="app.revanced.manager.permission.PLUGIN_HOST"
android:theme="@android:style/Theme.DeviceDefault" />
<meta-data
android:name="app.revanced.manager.plugin.downloader.class"
android:value="app.revanced.manager.plugin.downloader.example.ExamplePluginKt" />
</application>
</manifest>

View File

@ -1,69 +0,0 @@
@file:Suppress("Unused")
package app.revanced.manager.plugin.downloader.example
import android.app.Application
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Parcelable
import app.revanced.manager.plugin.downloader.Downloader
import app.revanced.manager.plugin.downloader.requestStartActivity
import app.revanced.manager.plugin.downloader.webview.WebViewDownloader
import kotlinx.parcelize.Parcelize
import kotlin.io.path.*
val apkMirrorDownloader = WebViewDownloader { packageName, version ->
with(Uri.Builder()) {
scheme("https")
authority("www.apkmirror.com")
mapOf(
"post_type" to "app_release",
"searchtype" to "apk",
"s" to (version?.let { "$packageName $it" } ?: packageName),
"bundles%5B%5D" to "apk_files" // bundles[]
).forEach { (key, value) ->
appendQueryParameter(key, value)
}
build().toString()
}
}
@Parcelize
class InstalledApp(val path: String) : Parcelable
private val application by lazy {
// Don't do this in a real plugin.
val clazz = Class.forName("android.app.ActivityThread")
val activityThread = clazz.getMethod("currentActivityThread")(null)
clazz.getMethod("getApplication")(activityThread) as Application
}
val installedAppDownloader = Downloader<InstalledApp> {
val pm = application.packageManager
get { packageName, version ->
val packageInfo = try {
pm.getPackageInfo(packageName, 0)
} catch (_: PackageManager.NameNotFoundException) {
return@get null
}
if (version != null && packageInfo.versionName != version) return@get null
requestStartActivity<InteractionActivity>()
InstalledApp(packageInfo.applicationInfo!!.sourceDir) to packageInfo.versionName
}
download { app ->
with(Path(app.path)) { inputStream() to fileSize() }
}
/*
download { app, outputStream ->
val path = Path(app.path)
reportSize(path.fileSize())
Files.copy(path, outputStream)
}*/
}

Some files were not shown because too many files have changed in this diff Show More