Compare commits

...

59 Commits

Author SHA1 Message Date
4e9f3fe1dc build: Bump version to v1.16.0 2023-11-21 17:36:24 +03:00
9bd48c19ff feat: ability to search query for suggested version (#1151)
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
Co-authored-by: Ushie <ushiekane@gmail.com>
2023-11-21 17:33:21 +03:00
c0516c3665 ci(build): Improve pull request build (#1305)
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
2023-11-20 14:50:44 +07:00
06ff36c836 feat: improve predictive back (#1487)
Signed-off-by: validcube <pun.butrach@gmail.com>
2023-11-20 14:49:39 +07:00
98c16eb1dc docs(readme): remove redundant feature information 2023-11-20 07:34:08 +07:00
35d80840e5 fix: Remove incorrect punctuation (#1499) 2023-11-19 05:38:34 +01:00
ad3a778fb9 refactor: apply lint from analyzer 2023-11-12 10:02:40 +05:45
d25d1efe9c fix(build): allow profile variant to compile
fix backported from #1305
2023-11-12 10:02:10 +05:45
de58dff423 docs: improve readme (#1484) 2023-11-12 10:02:07 +05:45
ec26e4d8d1 feat: update color scheme (#1483) 2023-11-12 09:59:55 +05:45
c24da41505 refactor: replace deprecated command 2023-11-12 09:59:50 +05:45
f785185e1c chore: update label for template 2023-11-11 13:19:01 +07:00
5ed3ed9a2d fix: patcher logs hiding behind navigation bar (#1476) 2023-11-11 12:00:21 +07:00
a3adace60e build: Update Gradle to v8.4 2023-11-08 17:22:06 +01:00
3db4de09e2 chore: Remove unnecessary project files 2023-11-08 17:21:02 +01:00
70b2ee0a84 feat: Disable selection of un-suggested app version by default (#1471)
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
2023-11-08 03:38:54 +01:00
e7d82850c9 fix: Log saved patch option values (#1420) 2023-11-07 23:37:59 +01:00
e6a8f4e6dc build: Bump version to v1.15.1 2023-11-04 22:41:48 +01:00
494e268bc5 fix: Use correct method name for string replacement 2023-11-04 22:41:27 +01:00
e1c6f65b7e chore: Merge branch dev to main (#1446) 2023-11-04 22:10:14 +01:00
89075c5588 build: Bump version to v1.15.0 2023-11-04 22:08:20 +01:00
c7fa9b8ce7 chore: upgrade dependencies (#1404) 2023-11-04 22:08:20 +01:00
cb70082d31 docs: Remove unneeded steps to build from source 2023-11-04 22:08:19 +01:00
48b9ac8f5b docs: use correct directory path (#1440) 2023-11-04 22:08:19 +01:00
0be568bbbd fix: Hide empty patches category labels (#1439) 2023-11-04 22:08:19 +01:00
ba44fa620f build: Bump dependencies to support patch option values (#1431)
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
2023-11-04 22:08:19 +01:00
dde402afbf chore: merge dev to main (#1437) 2023-10-26 17:22:07 +03:00
bbe5142ca9 build: bump version to v1.14.2 2023-10-26 17:21:25 +03:00
e74ffac5b0 fix: unable to use custom API (#1435) 2023-10-26 17:20:34 +03:00
cfb8980e3a chore: merge dev to main (#1425) 2023-10-25 11:42:54 +03:00
e06e1bdcbe build: bump version to v1.14.1 2023-10-25 11:42:19 +03:00
d60ced2f61 fix: remove codeblock from log export
Fixes: #1416
2023-10-25 11:40:30 +03:00
e68689828e fix: Material You toggle not updating sometimes (#1421) 2023-10-25 11:37:03 +03:00
ba932758c8 build: bump version to v1.14.0 2023-10-21 23:00:51 +07:00
ee43fa6311 chore: merge dev to main (#1399) 2023-10-21 22:58:58 +07:00
ad6b164d51 feat: root status in export patch log (#1407)
Co-authored-by: Ushie <ushiekane@gmail.com>
2023-10-21 17:46:51 +07:00
4a5510acb2 refactor: remove the remaining of semantic release
Removal reason: not maintained
2023-10-21 15:37:23 +07:00
970dbc4428 refactor(social): use correct github organisation name
the correct name is "ReVanced" not "revanced", we changed for a while now.
2023-10-21 15:33:36 +07:00
f8f37325eb refactor(social): change Twitter to X 2023-10-21 15:32:13 +07:00
bb999019ef feat: show patch options in error log (#1394) 2023-10-17 13:21:59 +07:00
533b6a155a feat: clarify "Version compatibility check" description (#1397) 2023-10-17 13:20:46 +07:00
4cdc92388c refactor: fix patch log order to be consistent with settings order (#1398) 2023-10-17 13:19:18 +07:00
ccc6be1e71 refactor(accessibility): improve patch options (#1369)
Co-authored-by: Palm <palmpasuthorn@gmail.com>
2023-10-16 16:47:59 +07:00
b355778a92 chore: merge dev to main (#1388) 2023-10-15 20:19:54 +03:00
6a12e8f37a build: bump version to v1.13.1 2023-10-15 20:19:23 +03:00
59adb91f5f fix(settings): inverted version compatibility switch 2023-10-15 20:16:58 +03:00
53677e2f39 fix: typo in reset patch selection dialog (#1387) 2023-10-15 20:14:19 +03:00
1c74f43b22 build: bump version to 1.13.0 2023-10-15 18:22:42 +07:00
b4801970e8 chore: merge dev to main (#1384)
ReVanced Manager 1.13.0!!
2023-10-15 18:18:09 +07:00
7a3a6b512f docs(building): use new highlight format 2023-10-15 18:04:19 +07:00
72ea33b6de fix(patch_selector): correct popup menu style
fix: #1372
2023-10-15 17:46:44 +07:00
d97192e0ee refactor(patch_options): disable card tap (#1368) 2023-10-15 16:56:43 +07:00
196d9fe4d2 refactor: reorganize and rename settings (#1307)
Co-authored-by: Pun Butrach <pun.butrach@gmail.com>
Co-authored-by: Ushie <ushiekane@gmail.com>
2023-10-15 16:56:02 +07:00
e960fcc303 feat: add user agent (#1380)
Co-authored-by: Pun Butrach <pun.butrach@gmail.com>
2023-10-15 16:51:31 +07:00
f334da95ff feat: Remove full external storage access (#1381) 2023-10-15 16:44:11 +07:00
5d5f311e36 fix(settings-view): list items jumping on scroll up (#1375) 2023-10-15 16:17:28 +07:00
d577e97758 fix(app-bar): title not hiding completely (#1376) 2023-10-15 16:16:55 +07:00
2dc92e26d3 fix(patches-selector): ignore punctuation marks when searching for patch (#1377) 2023-10-14 19:43:18 +07:00
f4994a36a3 chore: Merge branch dev to main (#1366) 2023-10-13 00:22:02 +02:00
97 changed files with 1956 additions and 8823 deletions

View File

@ -1,7 +1,7 @@
name: ⭐ Feature request
description: Create a new feature request.
title: 'feat: <title>'
labels: [feature-request]
labels: 'feature request'
body:
- type: markdown
attributes:

View File

@ -1,45 +1,121 @@
name: PR Build
on:
pull_request:
paths:
- ".github/workflows/pr-build.yml"
- "android/**"
- "assets/**"
- "lib/**"
workflow_dispatch:
inputs:
# Flutter
flutter-branch:
description: Flutter branch
type: choice
default: 'stable'
options:
- stable
- beta
- dev
- master
flutter-cache:
description: Cache
type: boolean
default: true
# Application configuration
app-flavour:
description: App flavour
default: 'release'
type: choice
options:
- release
- debug
- profile
# Pull Request
pr-number:
description: PR number (No hashtag)
required: true
run-name: "Build PR ${{ inputs.pr-number }}"
jobs:
build:
name: Build
name: Build the application
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- name: Setup
env:
GH_TOKEN: ${{ github.token }}
run: |
gh repo clone ${{ github.repository }}
cd revanced-manager
gh repo set-default ${{ github.repository }}
gh pr checkout ${{ inputs.pr-number }}
echo "DATETIME=$( TZ='UTC+0' date --rfc-email )" >> $GITHUB_ENV
echo "COMMIT_HASH=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
- name: Checkout
uses: actions/checkout@v4
with:
# Make sure the release step uses its own credentials:
# https://github.com/cycjimmy/semantic-release-action#private-packages
persist-credentials: false
fetch-depth: 0
- name: Setup JDK
uses: actions/setup-java@v3
with:
java-version: '11'
java-version: '17'
distribution: 'zulu'
cache: gradle
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
channel: 'stable'
cache: true
channel: ${{ inputs.flutter-branch }}
cache: ${{ inputs.flutter-cache }}
- name: Install Flutter dependencies
run: flutter pub get
- name: Generate files with Builder
run: flutter packages pub run build_runner build --delete-conflicting-outputs
run: dart run build_runner build --delete-conflicting-outputs
- name: Build with Flutter
env:
continue-on-error: true
id: flutter-build
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: flutter build apk --debug
run: |
flutter build apk --${{ inputs.app-flavour }};
- name: Prepare to comment
run: |
if [[ "${{ steps.flutter-build.outcome }}" == "success" ]]; then
echo "MESSAGE=✅ ReVanced Manager ${{ env.COMMIT_HASH }} build succeeded." >> $GITHUB_ENV
else
echo "MESSAGE=🚫 ReVanced Manager ${{ env.COMMIT_HASH }} build failed." >> $GITHUB_ENV
fi
- name: "Comment to Pull Request #${{ inputs.pr-number }}"
uses: thollander/actions-comment-pull-request@v2
with:
GITHUB_TOKEN: ${{ github.token }}
pr_number: ${{ inputs.pr-number }}
mode: recreate
message: |
## ⚒️ ReVanced PR Build workflow
${{ env.MESSAGE }}
You can see more details in run [${{ github.run_id }}](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})!
### ⚙️ Overview
- App flavor: ${{ inputs.app-flavour }}
- Branch: ${{ inputs.flutter-branch }}
- Start time: ${{ env.DATETIME }}
- name: Upload build
uses: actions/upload-artifact@v3
with:
name: revanced-manager
path: build/app/outputs/flutter-apk/app-debug.apk
if-no-files-found: error
name: revanced-manager-(${{ env.COMMIT_HASH }}-${{ inputs.pr-number }}-${{ inputs.app-flavour }})-${{ inputs.flutter-branch }}
path: |
build/app/outputs/flutter-apk/app-${{ inputs.app-flavour }}.apk
build/app/outputs/flutter-apk/app-${{ inputs.app-flavour }}.apk.sha1

View File

@ -12,10 +12,10 @@ jobs:
- uses: actions/checkout@v4
- name: Set env
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
- name: Set up JDK 11
- name: Set up JDK
uses: actions/setup-java@v3
with:
java-version: "11"
java-version: "17"
distribution: "zulu"
- uses: subosito/flutter-action@v2
with:
@ -23,7 +23,7 @@ jobs:
- name: Set up Flutter
run: flutter pub get
- name: Generate files with Builder
run: flutter packages pub run build_runner build --delete-conflicting-outputs
run: dart run build_runner build --delete-conflicting-outputs
- name: Build with Flutter
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -1,75 +0,0 @@
{
"branches": [
"main",
{
"name": "dev",
"prerelease": true
}
],
"plugins": [
"semantic-release-export-data",
"@semantic-release/commit-analyzer",
[
"@semantic-release/release-notes-generator",
{
"presetConfig": {
"types": [
{
"type": "build",
"section": "Dependency Updates"
},
{
"type": "chore",
"section": "Other Changes",
"hidden": false
},
{
"type": "perf",
"section": "Performance Improvements",
"hidden": false
},
{
"type": "refactor",
"section": "Code Improvements",
"hidden": false
}
]
}
}
],
"@semantic-release/changelog",
"semantic-release-flutter-plugin",
[
"@semantic-release/git",
{
"assets": [
"CHANGELOG.md",
"pubspec.yaml"
]
}
],
[
"@semantic-release/github",
{
"assets": [
{
"path": "build/app/outputs/apk/release/revanced-manager-*.apk"
}
],
"successComment": false
}
],
[
"@saithodev/semantic-release-backmerge",
{
"backmergeBranches": [
{
"from": "main",
"to": "dev"
}
],
"clearWorkspace": true
}
]
]
}

View File

@ -1,6 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="main.dart" type="FlutterRunConfigurationType" factoryName="Flutter">
<option name="filePath" value="$PROJECT_DIR$/lib/main.dart" />
<method v="2" />
</configuration>
</component>

View File

@ -1 +0,0 @@

88
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,88 @@
<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/">
<img height="24px" src="assets/revanced-logo/revanced-logo-round.svg" />
</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">
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
</a>&nbsp;&nbsp;&nbsp;
<a href="https://reddit.com/r/revancedapp">
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
</a>&nbsp;&nbsp;&nbsp;
<a href="https://t.me/app_revanced">
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
</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">
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
</a>
<br>
<br>
Continuing the legacy of Vanced
</p>
# 👋 Contribution guidelines
This document describes how to contribute to ReVanced Manager.
## 📖 Resources to help you get started
* The [documentation](/docs/README.md) provides steps to build ReVanced Manager from source
* Our [backlog](https://github.com/orgs/ReVanced/projects/12) is where we keep track of what we're working on
* [Issues](https://github.com/ReVanced/revanced-manager/issues) are where we keep track of bugs and feature requests
## 🙏 Submitting a feature request
Features can be requested by opening an issue using the
[feature request issue template](https://github.com/ReVanced/revanced-manager/issues/new?assignees=&labels=feature-request&projects=&template=feature-issue.yml&title=feat%3A+%3Ctitle%3E).
> [!NOTE]
> We may reject your request at the discretion of ReVanced Manager's maintainers,
> please provide good motivation for a request to be accepted.
## 🐞 Submitting a bug report
If you encounter a bug while using the ReVanced Manager app, open an issue using the
[bug report issue template](https://github.com/ReVanced/revanced-manager/issues/new?assignees=&labels=bug&projects=&template=bug-issue.yml&title=bug%3A+%3Ctitle%3E).
## 📝 How to contribute
> [!NOTE]
> 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 a new branch based off the `dev` branch
2. Commit your changes
3. Open a pull request to the `dev` branch and reference issues that your pull request closes
4. The maintainers of ReVanced Manager will review and provide suggestions.
Once your pull request is approved and merged, it will be included in the next release of ReVanced Manager
## 🤚 I want to contribute but don't know how to code
Even if you don't know how to code, you can still contribute by
translating ReVanced Manager on [Crowdin](https://translate.revanced.app/).
❤️ Thank you for considering contributing to ReVanced Manager.

104
README.md
View File

@ -1,35 +1,97 @@
<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-round.svg" />
<img height="24px" src="assets/revanced-logo/revanced-logo-round.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
The official ReVanced Manager based on Flutter.
[![GitHub last commit](https://img.shields.io/github/last-commit/ReVanced/revanced-manager)](https://github.com/ReVanced/revanced-manager/commits "")
[![GitHub commit activity](https://img.shields.io/github/commit-activity/w/ReVanced/revanced-manager)](https://github.com/ReVanced/revanced-manager/commits "")
ReVanced Manager is an Android application that uses ReVanced Patcher to add, remove, and modify existing functionalities in Android applications.
## 💪 Features
We provide the some of the features are:
* 📱 **Portable**: ReVanced Patcher that fit in your pocket;
* 🤗 **Intuitive UI**: Help you manage your patched applications with easy-to-use interface;
* 🛠️ **Customization**: Patch with third-party sources;
* ✨ And a **lot more!**
## 🔽 Download
You can obtain ReVanced Manager by downloading it from either [revanced.app/download](https://revanced.app/download) or [GitHub Releases](https://github.com/ReVanced/revanced-manager/releases)
You can get ReVanced Manager by downloading from [ReVanced site](https://revanced.app/download) or [GitHub releases](https://github.com/ReVanced/revanced-manager/releases).
## 📝 Prerequisites
## 📚 Everything else
1. Android 8 or higher
2. Incompatible with certain ARMv7 devices
### 📄 Documentation
## 📃 Documentation
The documentation can be found [here](https://github.com/revanced/revanced-manager/tree/main/docs).
Documentation on how to use the application is available [here](/docs/README.md).
## 🔴 Issues
### 👋 Contributing
For suggestions and bug reports, open an issue [here](https://github.com/revanced/revanced-manager/issues/new/choose).
Thank you for considering contributing to ReVanced Manager, you can find the contribution guidelines [here](/CONTRIBUTING.md).
## 🌐 Translation
### 🔴 Issues
[![Crowdin](https://badges.crowdin.net/revanced/localized.svg)](https://crowdin.com/project/revanced)
For suggestions and bug reports, open an issue [here](https://github.com/ReVanced/revanced-manager/issues/choose).
We're accepting translations on [Crowdin](https://translate.revanced.app).
## ⚖️ License
## 🛠️ Building Manager from source
1. Setup flutter environment for your [platform](https://docs.flutter.dev/get-started/install)
2. Clone the repository locally
3. Add your GitHub token in gradle.properties like [this](/docs/4_building.md)
4. Open the project in terminal
5. Run `flutter pub get` in terminal
6. Then `flutter packages pub run build_runner build --delete-conflicting-outputs` (Must be done on each git pull)
7. To build release APK run `flutter build apk`
ReVanced Manager adopts the [GNU General Public License 3.0](/LICENSE), [tl;dr](https://www.tldrlegal.com/license/gnu-general-public-license-v3-gpl-3): You may copy, distribute and modify the software as long as you track changes/dates in source files. Any modifications to or software including (via compiler) GPL-licensed code must also be made available under the GPL along with build & install instructions.

View File

@ -1,3 +1,9 @@
plugins {
id "com.android.application"
id "kotlin-android"
id "dev.flutter.flutter-gradle-plugin"
}
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
@ -6,11 +12,6 @@ if (localPropertiesFile.exists()) {
}
}
def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
flutterVersionCode = '1'
@ -21,35 +22,27 @@ if (flutterVersionName == null) {
flutterVersionName = '1.0'
}
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android {
compileSdkVersion flutter.compileSdkVersion
compileSdk 34
ndkVersion flutter.ndkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = '11'
jvmTarget = '17'
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
defaultConfig {
applicationId "app.revanced.manager.flutter"
minSdkVersion 26
targetSdkVersion 33
minSdk 26
targetSdk 34
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
buildTypes {
release {
shrinkResources false
@ -70,11 +63,32 @@ android {
abiFilters 'arm64-v8a', 'armeabi-v7a', 'x86_64'
}
}
profile {
shrinkResources false
minifyEnabled false
resValue "string", "app_name", "ReVanced Manager Profile"
applicationIdSuffix ".profile"
signingConfig signingConfigs.debug
ndk {
abiFilters 'arm64-v8a', 'armeabi-v7a', 'x86_64'
}
}
}
packagingOptions {
jniLibs {
useLegacyPackaging true
excludes += ['/prebuilt/**']
}
resources {
excludes += ['/prebuilt/**']
}
}
packagingOptions {
exclude '/prebuilt/**'
}
namespace 'app.revanced.manager.flutter'
}
kotlin {
jvmToolchain(17)
}
flutter {
@ -85,7 +99,7 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
// ReVanced
implementation "app.revanced:revanced-patcher:17.0.0"
implementation "app.revanced:revanced-patcher:19.0.0"
// Signing & aligning
implementation("org.bouncycastle:bcpkix-jdk15on:1.70")

View File

@ -1,4 +1,3 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="app.revanced.manager.flutter">
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@ -1,6 +1,4 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="app.revanced.manager.flutter">
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
@ -24,8 +22,7 @@
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
android:largeHeap="true"
android:requestLegacyExternalStorage="true"
android:extractNativeLibs="true">
android:requestLegacyExternalStorage="true">
<activity
android:name=".MainActivity"
android:exported="true"

View File

@ -1,5 +1,7 @@
package app.revanced.manager.flutter
import android.app.SearchManager
import android.content.Intent
import android.os.Handler
import android.os.Looper
import app.revanced.manager.flutter.utils.Aapt
@ -15,7 +17,9 @@ import app.revanced.patcher.patch.PatchResult
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import kotlinx.coroutines.InternalCoroutinesApi
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.runBlocking
import org.json.JSONArray
import org.json.JSONObject
@ -25,6 +29,7 @@ import java.io.StringWriter
import java.util.logging.LogRecord
import java.util.logging.Logger
class MainActivity : FlutterActivity() {
private val handler = Handler(Looper.getMainLooper())
private lateinit var installerChannel: MethodChannel
@ -38,6 +43,17 @@ class MainActivity : FlutterActivity() {
val patcherChannel = "app.revanced.manager.flutter/patcher"
val installerChannel = "app.revanced.manager.flutter/installer"
val openBrowserChannel = "app.revanced.manager.flutter/browser"
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, openBrowserChannel).setMethodCallHandler { call, result ->
if (call.method == "openBrowser") {
val searchQuery = call.argument<String>("query")
openBrowser(searchQuery)
result.success(null)
} else {
result.notImplemented()
}
}
val mainChannel =
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, patcherChannel)
@ -98,8 +114,10 @@ class MainActivity : FlutterActivity() {
val cacheDirPath = call.argument<String>("cacheDirPath")!!
try {
val patchBundleFile = File(patchBundleFilePath)
patchBundleFile.setWritable(false)
patches = PatchBundleLoader.Dex(
File(patchBundleFilePath),
patchBundleFile,
optimizedDexDirectory = File(cacheDirPath)
)
} catch (ex: Exception) {
@ -131,24 +149,34 @@ class MainActivity : FlutterActivity() {
})
put("options", JSONArray().apply {
it.options.values.forEach { option ->
val optionJson = JSONObject().apply option@{
JSONObject().apply {
put("key", option.key)
put("title", option.title)
put("description", option.description)
put("required", option.required)
when (val value = option.value) {
null -> put("value", null)
is Array<*> -> put("value", JSONArray().apply {
fun JSONObject.putValue(
value: Any?,
key: String = "value"
) = if (value is Array<*>) put(
key,
JSONArray().apply {
value.forEach { put(it) }
})
else -> put("value", option.value)
}
else put(key, value)
put("optionClassType", option::class.simpleName)
}
put(optionJson)
putValue(option.default)
option.values?.let { values ->
put("values",
JSONObject().apply {
values.forEach { (key, value) ->
putValue(value, key)
}
})
} ?: put("values", null)
put("valueType", option.valueType)
}.let(::put)
}
})
}.let(::put)
@ -161,6 +189,16 @@ class MainActivity : FlutterActivity() {
}
}
fun openBrowser(query: String?) {
val intent = Intent(Intent.ACTION_WEB_SEARCH).apply {
putExtra(SearchManager.QUERY, query)
}
if (intent.resolveActivity(packageManager) != null) {
startActivity(intent)
}
}
@OptIn(InternalCoroutinesApi::class)
private fun runPatcher(
result: MethodChannel.Result,
originalFilePath: String,
@ -283,12 +321,12 @@ class MainActivity : FlutterActivity() {
acceptPatches(patches)
runBlocking {
apply(false).collect { patchResult: PatchResult ->
apply(false).collect(FlowCollector { patchResult: PatchResult ->
if (cancel) {
handler.post { stopResult!!.success(null) }
this.cancel()
this@apply.close()
return@collect
return@FlowCollector
}
val msg = patchResult.exception?.let {
@ -301,7 +339,7 @@ class MainActivity : FlutterActivity() {
updateProgress(progress, "", msg)
progress += progressStep
}
})
}
}

View File

@ -1,4 +1,3 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="app.revanced.manager.flutter">
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@ -1,12 +1,12 @@
buildscript {
ext.kotlin_version = '1.9.0'
ext.kotlin_version = '1.9.10'
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.1.3'
classpath 'com.android.tools.build:gradle:8.1.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
@ -25,6 +25,8 @@ allprojects {
rootProject.buildDir = '../build'
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
project.evaluationDependsOn(':app')
}

View File

@ -4,3 +4,6 @@ org.gradle.daemon=true
org.gradle.caching=true
android.useAndroidX=true
android.enableJetifier=true
android.defaults.buildfeatures.buildconfig=true
android.nonTransitiveRClass=false
android.nonFinalResIds=false

View File

@ -1,5 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
networkTimeout=10000
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip

View File

@ -1,11 +1,20 @@
include ':app'
pluginManagement {
def flutterSdkPath = {
def properties = new Properties()
file("local.properties").withInputStream { properties.load(it) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
return flutterSdkPath
}
settings.ext.flutterSdkPath = flutterSdkPath()
def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
def properties = new Properties()
includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle")
assert localPropertiesFile.exists()
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
plugins {
id "dev.flutter.flutter-gradle-plugin" version "1.0.0" apply false
}
}
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
include ":app"
apply from: "${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle/app_plugin_loader.gradle"

View File

@ -1,6 +1,7 @@
{
"okButton": "OK",
"cancelButton": "Cancel",
"dismissButton": "Dismiss",
"quitButton": "Quit",
"updateButton": "Update",
"enabledLabel": "Enabled",
@ -110,6 +111,8 @@
"downloadToast": "Download function is not available yet",
"requireSuggestedAppVersionDialogText": "The version of the app you have selected does not match the suggested version. Please select the app that matches the suggested version.\n\nSelected version: v{selected}\nSuggested version: v{suggested}\n\nTo proceed anyway, disable \"Require suggested app version\" in the settings.",
"featureNotAvailable": "Feature not implemented",
"featureNotAvailableText": "This application is a split APK and cannot be selected. Unfortunately, this feature is only available for rooted users at the moment. However, you can still install the application by selecting its APK files from your device's storage instead"
},
@ -128,14 +131,14 @@
"none": "None",
"noneTooltip": "Deselect all patches",
"loadPatchesSelection": "Load patches selection",
"noSavedPatches": "No saved patches for the selected app.\nPress Done to save current selection.",
"loadPatchesSelection": "Load patch selection",
"noSavedPatches": "No saved patch selection for the selected app.\nPress Done to save the current selection.",
"noPatchesFound": "No patches found for the selected app",
"setRequiredOption": "Some patches require options to be set:\n\n{patches}\n\nPlease set them before continuing.",
"selectAllPatchesWarningContent": "You are about to select all patches, that includes non-suggested patches and can cause unwanted behavior."
"setRequiredOption": "Some patches require options to be set:\n\n{patches}\n\nPlease set them before continuing."
},
"patchOptionsView": {
"customValue": "Custom value",
"resetOptionsTooltip": "Reset patch options",
"viewTitle": "Patch options",
"saveOptions": "Save",
@ -152,10 +155,10 @@
},
"patchItem": {
"unsupportedDialogText": "Selecting this patch may result in patching errors.\n\nApp version: {packageVersion}\nSupported versions:\n{supportedVersions}",
"unsupportedPatchVersion": "Patch is not supported for this app version. Enable the experimental toggle in settings to proceed.",
"unsupportedPatchVersion": "Patch is not supported for this app version.",
"unsupportedRequiredOption": "This patch contains a required option that is not supported by this app",
"patchesChangeWarningDialogText": "It is recommended to use the default selection of patches because changing it may cause unexpected issues.\n\nIf you know what you are doing, you can enable \"Enable changing selection\" in the settings.",
"patchesChangeWarningDialogText": "It is recommended to use the default patch selection and options. Changing them may result in unexpected issues.\n\nYou'll need to turn on \"Allow changing patch selection\" in settings before changing any patch selection.",
"patchesChangeWarningDialogButton": "Use default selection"
},
"installerView": {
@ -187,10 +190,9 @@
"appearanceSectionTitle": "Appearance",
"teamSectionTitle": "Team",
"infoSectionTitle": "Info",
"debugSectionTitle": "Debugging",
"advancedSectionTitle": "Advanced",
"exportSectionTitle": "Import & export",
"logsSectionTitle": "Logs",
"themeModeLabel": "App theme",
"systemThemeLabel": "System",
@ -204,15 +206,15 @@
"englishOption": "English",
"sourcesLabel": "Sources",
"sourcesLabelHint": "Configure your custom sources",
"sourcesLabelHint": "Configure your sources",
"sourcesIntegrationsLabel": "Integrations source",
"sourcesResetDialogTitle": "Reset",
"sourcesResetDialogText": "Are you sure you want to reset custom sources to their default values?",
"apiURLResetDialogText": "Are you sure you want to reset API URL to its default value?",
"sourcesUpdateNote": "Note: ReVanced Patches will be updated to the latest version automatically.\n\nThis will reveal your IP address to the server.",
"sourcesResetDialogText": "Are you sure you want to reset your sources to their default values?",
"apiURLResetDialogText": "Are you sure you want to reset your API URL to its default value?",
"sourcesUpdateNote": "Note: Patches will be updated to the latest version automatically.\n\nThis will reveal your IP address to the server.",
"apiURLLabel": "API URL",
"apiURLHint": "Configure your custom API URL",
"apiURLHint": "Configure your API URL",
"selectApiURL": "API URL",
"hostRepositoryLabel": "Repository API",
"orgPatchesLabel": "Patches organization",
@ -222,21 +224,24 @@
"contributorsLabel": "Contributors",
"contributorsHint": "A list of contributors of ReVanced",
"logsLabel": "Logs",
"logsHint": "Share Manager's logs",
"logsLabel": "Share logs",
"logsHint": "Share ReVanced Manager logs",
"enablePatchesSelectionLabel": "Enable changing selection",
"enablePatchesSelectionHint": "Enable changing the selection of patches.",
"enablePatchesSelectionWarningText": "Changing the default selection of patches may cause unexpected issues.\n\nEnable anyways?",
"enablePatchesSelectionLabel": "Allow changing patch selection",
"enablePatchesSelectionHint": "Allow changing the selection of patches",
"enablePatchesSelectionWarningText": "Changing the selection of patches may cause unexpected issues.\n\nEnable anyways?",
"disablePatchesSelectionWarningText": "You are about to disable changing the selection of patches.\nThe default selection of patches will be restored.\n\nDisable anyways?",
"autoUpdatePatchesLabel": "Auto update patches",
"autoUpdatePatchesHint": "Automatically update ReVanced Patches to the latest version",
"experimentalUniversalPatchesLabel": "Experimental universal patches support",
"experimentalUniversalPatchesHint": "Display all applications to use with universal patches, loading list of apps may be slower",
"experimentalPatchesLabel": "Experimental patches support",
"experimentalPatchesHint": "Enable usage of unsupported patches in any app version",
"enabledExperimentalPatches": "Experimental patches support enabled",
"autoUpdatePatchesHint": "Automatically update patches to the latest version",
"universalPatchesLabel": "Show universal patches",
"universalPatchesHint": "Display all apps and universal patches (may slow down the app list)",
"versionCompatibilityCheckLabel": "Version compatibility check",
"versionCompatibilityCheckHint": "Restricts patches to supported app versions",
"requireSuggestedAppVersionLabel": "Require suggested app version",
"requireSuggestedAppVersionHint": "Enforce selection of suggested app version",
"requireSuggestedAppVersionDialogText": "Selecting an app that is not the suggested version may cause unexpected issues.\n\nDo you want to proceed anyways?",
"aboutLabel": "About",
"snackbarMessage": "Copied to clipboard",
@ -246,54 +251,52 @@
"deleteTempDirHint": "Delete unused temporary files",
"deletedTempDir": "Temporary files deleted",
"exportPatchesLabel": "Export patches selection",
"exportPatchesHint": "Export patches selection to a JSON file",
"exportedPatches": "Patches selection exported",
"noExportFileFound": "No patches selection to export",
"exportPatchesLabel": "Export patch selection",
"exportPatchesHint": "Export patch selection to a JSON file",
"exportedPatches": "Patch selection exported",
"noExportFileFound": "No patch selection to export",
"importPatchesLabel": "Import patches selection",
"importPatchesHint": "Import patches selection from a JSON file",
"importedPatches": "Patches selection imported",
"importPatchesLabel": "Import patch selection",
"importPatchesHint": "Import patch selection from a JSON file",
"importedPatches": "Patch selection imported",
"resetStoredPatchesLabel": "Reset patches",
"resetStoredPatchesHint": "Reset the stored patches selection",
"resetStoredPatchesLabel": "Reset patch selection",
"resetStoredPatchesHint": "Reset the stored patch selection",
"resetStoredPatchesDialogTitle": "Reset patch selection?",
"resetStoredPatchesDialogText": "The default selection of patches will be restored.",
"resetStoredPatches": "Patch selection has been reset",
"resetStoredOptionsLabel": "Reset options",
"resetStoredOptionsLabel": "Reset patch options",
"resetStoredOptionsHint": "Reset all patch options",
"resetStoredPatchesDialogTitle": "Reset patches selection?",
"resetStoredPatchesDialogText": "Resetting patches selection will remove all selected patches.",
"resetStoredPatches": "Patches selection has been reset",
"resetStoredOptionsDialogTitle": "Reset options?",
"resetStoredOptionsDialogText": "Resetting options will remove all saved options.",
"resetStoredOptionsDialogTitle": "Reset patch options?",
"resetStoredOptionsDialogText": "Resetting patch options will remove all saved options.",
"resetStoredOptions": "Options have been reset",
"deleteLogsLabel": "Delete logs",
"deleteLogsHint": "Delete collected manager logs",
"deleteLogsLabel": "Clear logs",
"deleteLogsHint": "Delete collected ReVanced Manager logs",
"deletedLogs": "Logs deleted",
"regenerateKeystoreLabel": "Regenerate keystore",
"regenerateKeystoreHint": "Regenerate the keystore used to sign the app",
"regenerateKeystoreHint": "Regenerate the keystore used to sign apps",
"regenerateKeystoreDialogTitle": "Regenerate keystore?",
"regenerateKeystoreDialogText": "Patched apps signed with the old keystore will no longer be able to update.",
"regenerateKeystoreDialogText": "Patched apps signed with the old keystore will no longer be able to be updated.",
"regeneratedKeystore": "Keystore regenerated",
"exportKeystoreLabel": "Export keystore",
"exportKeystoreHint": "Export keystore used to sign apps",
"exportKeystoreHint": "Export the keystore used to sign apps",
"exportedKeystore": "Keystore exported",
"noKeystoreExportFileFound": "No keystore to export",
"importKeystoreLabel": "Import keystore",
"importKeystoreHint": "Import keystore used to sign apps",
"importKeystoreHint": "Import a keystore used to sign apps",
"importedKeystore": "Keystore imported",
"selectKeystorePassword": "Keystore Password",
"selectKeystorePasswordHint": "Select keystore password used to sign the apk",
"selectKeystorePassword": "Keystore password",
"selectKeystorePasswordHint": "Select keystore password used to sign apps",
"jsonSelectorErrorMessage": "Unable to use selected JSON file",
"keystoreSelectorErrorMessage": "Unable to use selected KEYSTORE file"
"keystoreSelectorErrorMessage": "Unable to use selected keystore file"
},
"appInfoView": {
"widgetTitle": "App info",

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 800 800" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><g id="Logo"><g id="Ring"><circle id="Ring-Background" serif:id="Ring Background" cx="400" cy="400" r="400" style="fill:#1b1b1b;"/><path id="Ring1" serif:id="Ring" d="M400,0c220.766,0 400,179.234 400,400c-0,220.766 -179.234,400 -400,400c-220.766,-0 -400,-179.234 -400,-400c0,-220.766 179.234,-400 400,-400Zm-0,36c200.897,-0 364,163.103 364,364c0,200.897 -163.103,364 -364,364c-200.897,0 -364,-163.103 -364,-364c-0,-200.897 163.103,-364 364,-364Z" style="fill:url(#_Linear1);"/></g><g id="Shape"><path id="V-Shape" serif:id="V Shape" d="M538.74,269.872c1.481,-3.382 1.157,-7.283 -0.863,-10.373c-2.021,-3.091 -5.464,-4.954 -9.156,-4.954c-5.148,0 -10.435,0 -14.165,0c-3.1,0 -5.907,1.834 -7.153,4.672c-12.468,28.396 -78.273,178.273 -100.25,228.328c-1.246,2.838 -4.053,4.671 -7.154,4.671c-3.1,0 -5.907,-1.833 -7.153,-4.671c-21.977,-50.055 -87.782,-199.932 -100.25,-228.328c-1.246,-2.838 -4.053,-4.672 -7.153,-4.672c-3.73,0 -9.017,0 -14.164,0c-3.693,0 -7.135,1.863 -9.156,4.954c-2.02,3.09 -2.344,6.991 -0.863,10.373c23.557,53.766 101.872,232.519 117.871,269.034c1.743,3.979 5.674,6.549 10.018,6.549c6.293,-0 15.408,-0 21.701,-0c4.344,-0 8.275,-2.57 10.018,-6.549c15.999,-36.515 94.315,-215.268 117.872,-269.034Z" style="fill:#fff;"/><path id="Diamond" d="M408.119,395.312c-1.675,2.901 -4.77,4.688 -8.119,4.688c-3.349,-0 -6.444,-1.787 -8.119,-4.688c-16.997,-29.44 -56.156,-97.264 -73.153,-126.704c-1.675,-2.901 -1.675,-6.474 0,-9.375c1.675,-2.901 4.77,-4.688 8.119,-4.688c33.995,0 112.311,0 146.306,0c3.349,0 6.444,1.787 8.119,4.688c1.675,2.901 1.675,6.474 -0,9.375c-16.997,29.44 -56.156,97.264 -73.153,126.704Z" style="fill:url(#_Linear2);"/></g></g><defs><linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(4.89859e-14,800,-800,4.89859e-14,400.001,3.31681e-10)"><stop offset="0" style="stop-color:#f04e98;stop-opacity:1"/><stop offset="0.5" style="stop-color:#5f65d4;stop-opacity:1"/><stop offset="1" style="stop-color:#4e98f0;stop-opacity:1"/></linearGradient><linearGradient id="_Linear2" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.77155e-14,289.317,-282.535,1.73003e-14,400,254.545)"><stop offset="0" style="stop-color:#f04e98;stop-opacity:1"/><stop offset="0.5" style="stop-color:#5f65d4;stop-opacity:1"/><stop offset="1" style="stop-color:#4e98f0;stop-opacity:1"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -2,37 +2,41 @@
ReVanced Manager has settings that can be configured to your liking.
## Essential settings
## 🎛️ Essential settings
- ### 🔗 API URL
- ### 🪛 Allow changing patch selection
API to use to fetch updates and ReVanced Patches from.
Allows the user to change the patch selection from the default selection.
- ### 🔍 Version compatibility check
Constrains patches to supported app versions. Disable this to patch any version of an app.
> [!WARNING]
> Disabling this may cause issues if the patches are not compatible with the app version.
- ### 🧑‍🔬 Show universal patches
Reveals patches which can be applied to any app.
> [!WARNING]
> These patches may not work on all apps.
- ### 🧬 Sources
Override the API and download ReVanced Patches from a different source.
Override the API and download patches from a different source.
- ### 🧪 Experimental ReVanced Patches support
- ### 🔗 API URL
Disable checking for the version of the app when applying ReVanced Patches.
> [!WARNING]
> This may cause issues if the ReVanced Patches are not compatible with the app version.
- ### 🧑‍🔬 Experimental universal support
This will show or hide ReVanced Patches, which are not meant for any app in particular but apply to all apps
> [!WARNING]
> Because the patches generalize the app, they may not work on all apps.
API to use to fetch updates and patches from.
- ### 💾 Imports & Exports
You can import, export or reset the following settings:
- 🔑 Keystore
- 📄 ReVanced Patches selection
- ⚙️ Options
- 📄 Patch selection
- ⚙️ Patch options
> [!NOTE]
> This is particularly useful if you want to backup or reset your settings.
@ -45,4 +49,4 @@ ReVanced Manager has settings that can be configured to your liking.
The next page will bring you back to the usage page.
Continue: [🛠️ Usage](2_usage.md)
Continue: [🛠️ Usage](2_usage.md)

View File

@ -9,31 +9,22 @@ This page will guide you through building ReVanced Manager from source.
```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 `~/android/gradle.properties`
```properties
gpr.user = YourUsername
gpr.key = ghp_longrandomkey
```
5. Get dependencies
3. Get dependencies
```sh
flutter pub get
```
6. Delete conflicting outputs
4. Delete conflicting outputs
```sh
flutter packages pub run build_runner build --delete-conflicting-outputs
dart run build_runner build --delete-conflicting-outputs
```
> **Note**: Must be run every time you sync your local repository with the remote repository.
> [!Note]
> Must be run every time you sync your local repository with the remote repository.
7. Build the APK
5. Build the APK
```sh
flutter build apk

BIN
fonts/custom-icons.ttf Normal file

Binary file not shown.

Binary file not shown.

View File

@ -1,5 +0,0 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

240
gradlew vendored
View File

@ -1,240 +0,0 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

91
gradlew.bat vendored
View File

@ -1,91 +0,0 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@ -1,3 +1,4 @@
import 'package:revanced_manager/services/download_manager.dart';
import 'package:revanced_manager/services/github_api.dart';
import 'package:revanced_manager/services/manager_api.dart';
import 'package:revanced_manager/services/patcher_api.dart';
@ -41,6 +42,7 @@ import 'package:stacked_services/stacked_services.dart';
LazySingleton(classType: PatcherAPI),
LazySingleton(classType: RevancedAPI),
LazySingleton(classType: GithubAPI),
LazySingleton(classType: DownloadManager),
LazySingleton(classType: Toast),
],
)

View File

@ -4,21 +4,21 @@ import 'package:flutter/material.dart';
import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/services/download_manager.dart';
import 'package:revanced_manager/services/github_api.dart';
import 'package:revanced_manager/services/manager_api.dart';
import 'package:revanced_manager/services/revanced_api.dart';
import 'package:revanced_manager/ui/theme/dynamic_theme_builder.dart';
import 'package:revanced_manager/ui/views/navigation/navigation_view.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:stacked_themes/stacked_themes.dart';
import 'package:timezone/data/latest.dart' as tz;
late SharedPreferences prefs;
Future main() async {
await ThemeManager.initialise();
await setupLocator();
WidgetsFlutterBinding.ensureInitialized();
await locator<ManagerAPI>().initialize();
await locator<DownloadManager>().initialize();
final String apiUrl = locator<ManagerAPI>().getApiUrl();
await locator<RevancedAPI>().initialize(apiUrl);
final String repoUrl = locator<ManagerAPI>().getRepoUrl();
@ -30,7 +30,7 @@ Future main() async {
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
const MyApp({super.key});
@override
Widget build(BuildContext context) {

View File

@ -13,12 +13,15 @@ class Patch {
});
factory Patch.fromJson(Map<String, dynamic> json) {
// See: https://github.com/ReVanced/revanced-manager/issues/1364#issuecomment-1760414618
_migrateV16ToV17(json);
return _$PatchFromJson(json);
}
static void _migrateV16ToV17(Map<String, dynamic> json) {
if (json['options'] == null) {
json['options'] = [];
}
return _$PatchFromJson(json);
}
final String name;
@ -57,18 +60,37 @@ class Option {
required this.title,
required this.description,
required this.value,
required this.values,
required this.required,
required this.optionClassType,
required this.valueType,
});
factory Option.fromJson(Map<String, dynamic> json) => _$OptionFromJson(json);
factory Option.fromJson(Map<String, dynamic> json) {
_migrateV17ToV19(json);
return _$OptionFromJson(json);
}
static void _migrateV17ToV19(Map<String, dynamic> json) {
if (json['valueType'] == null) {
final type = json['optionClassType'];
if (type is String) {
json['valueType'] = type
.replaceAll('PatchOption', '')
.replaceAll('List', 'Array');
json['optionClassType'] = null;
}
}
}
final String key;
final String title;
final String description;
dynamic value;
final dynamic value;
final Map<String, dynamic>? values;
final bool required;
final String optionClassType;
final String valueType;
Map toJson() => _$OptionToJson(this);
}

View File

@ -0,0 +1,75 @@
import 'package:dio/dio.dart';
import 'package:dio_cache_interceptor/dio_cache_interceptor.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_cache_manager/file.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:injectable/injectable.dart';
import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/services/manager_api.dart';
@lazySingleton
class DownloadManager {
final ManagerAPI _managerAPI = locator<ManagerAPI>();
late final String _userAgent;
final _cacheOptions = CacheOptions(
store: MemCacheStore(),
maxStale: const Duration(days: 1),
priority: CachePriority.high,
);
Future<void> initialize() async {
_userAgent = 'ReVanced-Manager/${await _managerAPI.getCurrentManagerVersion()}';
}
Dio initDio(String url) {
var dio = Dio();
try {
dio = Dio(
BaseOptions(
baseUrl: url,
headers: {
'User-Agent': _userAgent,
},
),
);
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
}
dio.interceptors.add(DioCacheInterceptor(options: _cacheOptions));
return dio;
}
Future<void> clearAllCache() async {
try {
await _cacheOptions.store!.clean();
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
}
}
Future<File> getSingleFile(String url) async {
return DefaultCacheManager().getSingleFile(
url,
headers: {
'User-Agent': _userAgent,
},
);
}
Stream<FileResponse> getFileStream(String url) {
return DefaultCacheManager().getFileStream(
url,
withProgress: true,
headers: {
'User-Agent': _userAgent,
},
);
}
}

View File

@ -1,48 +1,24 @@
import 'dart:io';
import 'package:collection/collection.dart';
import 'package:dio/dio.dart';
import 'package:dio_cache_interceptor/dio_cache_interceptor.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:injectable/injectable.dart';
import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/services/download_manager.dart';
import 'package:revanced_manager/services/manager_api.dart';
@lazySingleton
class GithubAPI {
late Dio _dio = Dio();
late final Dio _dio;
late final ManagerAPI _managerAPI = locator<ManagerAPI>();
final _cacheOptions = CacheOptions(
store: MemCacheStore(),
maxStale: const Duration(days: 1),
priority: CachePriority.high,
);
late final DownloadManager _downloadManager = locator<DownloadManager>();
Future<void> initialize(String repoUrl) async {
try {
_dio = Dio(
BaseOptions(
baseUrl: repoUrl,
),
);
_dio.interceptors.add(DioCacheInterceptor(options: _cacheOptions));
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
}
_dio = _downloadManager.initDio(repoUrl);
}
Future<void> clearAllCache() async {
try {
await _cacheOptions.store!.clean();
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
}
await _downloadManager.clearAllCache();
}
Future<Map<String, dynamic>?> getLatestRelease(
@ -104,7 +80,7 @@ class GithubAPI {
final Map<String, dynamic> releases = response.data[0];
int updates = 0;
final String currentVersion =
await ManagerAPI().getCurrentManagerVersion();
await _managerAPI.getCurrentManagerVersion();
while (response.data[updates]['tag_name'] != 'v$currentVersion') {
updates++;
}
@ -141,7 +117,7 @@ class GithubAPI {
(asset) => (asset['name'] as String).endsWith(extension),
);
if (asset != null) {
return await DefaultCacheManager().getSingleFile(
return await _downloadManager.getSingleFile(
asset['browser_download_url'],
);
}
@ -162,7 +138,7 @@ class GithubAPI {
) async {
try {
if (url.isNotEmpty) {
return await DefaultCacheManager().getSingleFile(
return await _downloadManager.getSingleFile(
url,
);
}
@ -180,7 +156,7 @@ class GithubAPI {
} else {
_managerAPI.setPatchesDownloadURL(downloadUrl);
}
return await DefaultCacheManager().getSingleFile(
return await _downloadManager.getSingleFile(
downloadUrl,
);
}

View File

@ -1,6 +1,7 @@
import 'dart:convert';
import 'dart:io';
import 'package:device_apps/device_apps.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_i18n/widgets/I18nText.dart';
@ -33,6 +34,8 @@ class ManagerAPI {
Patch? selectedPatch;
BuildContext? ctx;
bool isRooted = false;
bool suggestedAppVersionSelected = true;
bool isDynamicThemeAvailable = false;
String storedPatchesFile = '/selected-patches.json';
String keystoreFile =
'/sdcard/Android/data/app.revanced.manager.flutter/files/revanced-manager.keystore';
@ -59,10 +62,16 @@ class ManagerAPI {
Future<void> initialize() async {
_prefs = await SharedPreferences.getInstance();
isRooted = await _rootAPI.isRooted();
isDynamicThemeAvailable = (await getSdkVersion()) >= 31; // ANDROID_12_SDK_VERSION = 31
storedPatchesFile =
(await getApplicationDocumentsDirectory()).path + storedPatchesFile;
}
Future<int> getSdkVersion() async {
final AndroidDeviceInfo info = await DeviceInfoPlugin().androidInfo;
return info.version.sdkInt;
}
String getApiUrl() {
return _prefs.getString('apiUrl') ?? defaultApiUrl;
}
@ -71,7 +80,6 @@ class ManagerAPI {
if (url.isEmpty || url == ' ') {
url = defaultApiUrl;
}
await _revancedAPI.initialize(url);
await _revancedAPI.clearAllCache();
await _prefs.setString('apiUrl', url);
}
@ -244,12 +252,20 @@ class ManagerAPI {
await _prefs.setBool('universalPatchesEnabled', value);
}
bool areExperimentalPatchesEnabled() {
return _prefs.getBool('experimentalPatchesEnabled') ?? false;
bool isVersionCompatibilityCheckEnabled() {
return _prefs.getBool('versionCompatibilityCheckEnabled') ?? true;
}
Future<void> enableExperimentalPatchesStatus(bool value) async {
await _prefs.setBool('experimentalPatchesEnabled', value);
Future<void> enableVersionCompatibilityCheckStatus(bool value) async {
await _prefs.setBool('versionCompatibilityCheckEnabled', value);
}
bool isRequireSuggestedAppVersionEnabled() {
return _prefs.getBool('requireSuggestedAppVersionEnabled') ?? true;
}
Future<void> enableRequireSuggestedAppVersionStatus(bool value) async {
await _prefs.setBool('requireSuggestedAppVersionEnabled', value);
}
Future<void> setKeystorePassword(String password) async {
@ -566,8 +582,8 @@ class ManagerAPI {
return showDialog(
barrierDismissible: false,
context: context,
builder: (context) => WillPopScope(
onWillPop: () async => false,
builder: (context) => PopScope(
canPop: false,
child: AlertDialog(
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
title: I18nText('warning'),
@ -677,7 +693,7 @@ class ManagerAPI {
Future<List<String>> getDefaultPatches() async {
final List<Patch> patches = await getPatches();
final List<String> defaultPatches = [];
if (areExperimentalPatchesEnabled() == false) {
if (isVersionCompatibilityCheckEnabled() == true) {
defaultPatches.addAll(
patches
.where(

View File

@ -1,10 +1,10 @@
import 'dart:io';
import 'package:collection/collection.dart';
import 'package:cr_file_saver/file_saver.dart';
import 'package:device_apps/device_apps.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter_file_dialog/flutter_file_dialog.dart';
import 'package:injectable/injectable.dart';
import 'package:install_plugin/install_plugin.dart';
import 'package:path_provider/path_provider.dart';
@ -13,7 +13,7 @@ import 'package:revanced_manager/models/patch.dart';
import 'package:revanced_manager/models/patched_application.dart';
import 'package:revanced_manager/services/manager_api.dart';
import 'package:revanced_manager/services/root_api.dart';
import 'package:share_extend/share_extend.dart';
import 'package:share_plus/share_plus.dart';
@lazySingleton
class PatcherAPI {
@ -236,10 +236,10 @@ void exportPatchedFile(String appName, String version) {
try {
if (outFile != null) {
final String newName = _getFileName(appName, version);
CRFileSaver.saveFileWithDialog(
SaveFileDialogParams(
FlutterFileDialog.saveFile(
params: SaveFileDialogParams(
sourceFilePath: outFile!.path,
destinationFileName: newName,
fileName: newName,
),
);
}
@ -258,7 +258,7 @@ void sharePatchedFile(String appName, String version) {
final String newPath =
outFile!.path.substring(0, lastSeparator + 1) + newName;
final File shareFile = outFile!.copySync(newPath);
ShareExtend.share(shareFile.path, 'file');
Share.shareXFiles([XFile(shareFile.path)]);
}
} on Exception catch (e) {
if (kDebugMode) {
@ -286,10 +286,10 @@ Future<void> exportPatcherLog(String logs) async {
final String fileName = 'revanced-manager_patcher_$dateTime.txt';
final File log = File('${logDir.path}/$fileName');
log.writeAsStringSync(logs);
CRFileSaver.saveFileWithDialog(
SaveFileDialogParams(
FlutterFileDialog.saveFile(
params: SaveFileDialogParams(
sourceFilePath: log.path,
destinationFileName: fileName,
fileName: fileName,
),
);
}

View File

@ -3,49 +3,27 @@ import 'dart:io';
import 'package:collection/collection.dart';
import 'package:dio/dio.dart';
import 'package:dio_cache_interceptor/dio_cache_interceptor.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:injectable/injectable.dart';
import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/services/download_manager.dart';
import 'package:synchronized/synchronized.dart';
import 'package:timeago/timeago.dart';
@lazySingleton
class RevancedAPI {
late Dio _dio = Dio();
late final Dio _dio;
late final DownloadManager _downloadManager = locator<DownloadManager>();
final Lock getToolsLock = Lock();
final _cacheOptions = CacheOptions(
store: MemCacheStore(),
maxStale: const Duration(days: 1),
priority: CachePriority.high,
);
Future<void> initialize(String apiUrl) async {
try {
_dio = Dio(
BaseOptions(
baseUrl: apiUrl,
),
);
_dio.interceptors.add(DioCacheInterceptor(options: _cacheOptions));
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
}
Future<void> initialize(String repoUrl) async {
_dio = _downloadManager.initDio(repoUrl);
}
Future<void> clearAllCache() async {
try {
await _cacheOptions.store!.clean();
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
}
await _downloadManager.clearAllCache();
}
Future<Map<String, List<dynamic>>> getContributors() async {
@ -120,7 +98,7 @@ class RevancedAPI {
);
if (release != null) {
final String url = release['browser_download_url'];
return await DefaultCacheManager().getSingleFile(url);
return await _downloadManager.getSingleFile(url);
}
} on Exception catch (e) {
if (kDebugMode) {
@ -152,9 +130,8 @@ class RevancedAPI {
'revanced/revanced-manager',
);
File? outputFile;
await for (final result in DefaultCacheManager().getFileStream(
await for (final result in _downloadManager.getFileStream(
release!['browser_download_url'] as String,
withProgress: true,
)) {
if (result is DownloadProgress) {
final totalSize = result.totalSize ?? 10000000;

View File

@ -2,8 +2,8 @@ import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
var lightCustomColorScheme = ColorScheme.fromSeed(
seedColor: Colors.blue,
primary: const Color(0xff1B73E8),
seedColor: Colors.purple,
primary: const Color(0xFF4C51C0),
);
var lightCustomTheme = ThemeData(
@ -21,10 +21,10 @@ var lightCustomTheme = ThemeData(
);
var darkCustomColorScheme = ColorScheme.fromSeed(
seedColor: Colors.blue,
seedColor: Colors.purple,
brightness: Brightness.dark,
primary: const Color(0xffA5CAFF),
surface: const Color(0xff1B1A1D),
primary: const Color(0xFFBFC1FF),
surface: const Color(0xFF131316),
);
var darkCustomTheme = ThemeData(
@ -38,7 +38,7 @@ var darkCustomTheme = ThemeData(
),
),
),
canvasColor: const Color(0xff1B1A1D),
scaffoldBackgroundColor: const Color(0xff1B1A1D),
canvasColor: const Color(0xFF131316),
scaffoldBackgroundColor: const Color(0xFF131316),
textTheme: GoogleFonts.robotoTextTheme(ThemeData.dark().textTheme),
);

View File

@ -12,11 +12,11 @@ import 'package:stacked_services/stacked_services.dart';
class DynamicThemeBuilder extends StatefulWidget {
const DynamicThemeBuilder({
Key? key,
super.key,
required this.title,
required this.home,
required this.localizationsDelegates,
}) : super(key: key);
});
final String title;
final Widget home;
final Iterable<LocalizationsDelegate> localizationsDelegates;

View File

@ -8,7 +8,7 @@ import 'package:revanced_manager/ui/widgets/shared/search_bar.dart';
import 'package:stacked/stacked.dart' hide SkeletonLoader;
class AppSelectorView extends StatefulWidget {
const AppSelectorView({Key? key}) : super(key: key);
const AppSelectorView({super.key});
@override
State<AppSelectorView> createState() => _AppSelectorViewState();
@ -28,7 +28,6 @@ class _AppSelectorViewState extends State<AppSelectorView> {
icon: const Icon(Icons.sd_storage),
onPressed: () {
model.selectAppFromStorage(context);
Navigator.of(context).pop();
},
),
body: CustomScrollView(
@ -38,13 +37,11 @@ class _AppSelectorViewState extends State<AppSelectorView> {
floating: true,
title: I18nText(
'appSelectorView.viewTitle',
child: Text(
'',
style: TextStyle(
),
titleTextStyle: TextStyle(
fontSize: 22.0,
color: Theme.of(context).textTheme.titleLarge!.color,
),
),
),
leading: IconButton(
icon: Icon(
Icons.arrow_back,
@ -115,9 +112,13 @@ class _AppSelectorViewState extends State<AppSelectorView> {
context,
app.packageName,
),
onLinkTap: () =>
model.searchSuggestedVersionOnWeb(
packageName: app.packageName,
),
),
)
.toList(),
,
...model
.getFilteredAppsNames(_query)
.map(
@ -129,9 +130,13 @@ class _AppSelectorViewState extends State<AppSelectorView> {
onTap: () {
model.showDownloadToast();
},
onLinkTap: () =>
model.searchSuggestedVersionOnWeb(
packageName: app,
),
),
)
.toList(),
,
const SizedBox(height: 70.0),
],
),

View File

@ -1,9 +1,10 @@
import 'dart:io';
import 'package:device_apps/device_apps.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_file_dialog/flutter_file_dialog.dart';
import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/models/patch.dart';
@ -70,7 +71,53 @@ class AppSelectorViewModel extends BaseViewModel {
return true;
}
Future<void> selectApp(ApplicationWithIcon application) async {
Future<void> searchSuggestedVersionOnWeb({
required String packageName,
}) async {
final String suggestedVersion = getSuggestedVersion(packageName);
if (suggestedVersion.isNotEmpty) {
await openDefaultBrowser('$packageName apk version v$suggestedVersion');
} else {
await openDefaultBrowser('$packageName apk');
}
}
Future<void> openDefaultBrowser(String query) async {
if (Platform.isAndroid) {
try {
const platform = MethodChannel('app.revanced.manager.flutter/browser');
await platform.invokeMethod('openBrowser', {'query': query});
} catch (e) {
if (kDebugMode) {
print(e);
}
}
} else {
throw 'Platform not supported';
}
}
Future<void> selectApp(
BuildContext context,
ApplicationWithIcon application, [
bool isFromStorage = false,
]) async {
final String suggestedVersion =
getSuggestedVersion(application.packageName);
if (application.versionName != suggestedVersion && suggestedVersion.isNotEmpty) {
_managerAPI.suggestedAppVersionSelected = false;
if (_managerAPI.isRequireSuggestedAppVersionEnabled() &&
context.mounted) {
return showRequireSuggestedAppVersionDialog(
context,
application.versionName!,
suggestedVersion,
);
}
} else {
_managerAPI.suggestedAppVersionSelected = true;
}
locator<PatcherViewModel>().selectedApp = PatchedApplication(
name: application.appName,
packageName: application.packageName,
@ -78,8 +125,12 @@ class AppSelectorViewModel extends BaseViewModel {
apkFilePath: application.apkFilePath,
icon: application.icon,
patchDate: DateTime.now(),
isFromStorage: isFromStorage,
);
await locator<PatcherViewModel>().loadLastSelectedPatches();
if (context.mounted) {
Navigator.pop(context);
}
}
Future<void> canSelectInstalled(
@ -89,23 +140,60 @@ class AppSelectorViewModel extends BaseViewModel {
final app =
await DeviceApps.getApp(packageName, true) as ApplicationWithIcon?;
if (app != null) {
if (await checkSplitApk(packageName) && !isRooted) {
final bool isSplitApk = await checkSplitApk(packageName);
if (isRooted || !isSplitApk) {
if (context.mounted) {
await selectApp(context, app);
}
final List<Option> requiredNullOptions = getNullRequiredOptions(
locator<PatcherViewModel>().selectedPatches,
packageName,
);
if (requiredNullOptions.isNotEmpty) {
locator<PatcherViewModel>().showRequiredOptionDialog();
}
} else {
if (context.mounted) {
return showSelectFromStorageDialog(context);
}
} else if (!await checkSplitApk(packageName) || isRooted) {
await selectApp(app);
if (context.mounted) {
Navigator.pop(context);
}
final List<Option> requiredNullOptions = getNullRequiredOptions(locator<PatcherViewModel>().selectedPatches, packageName);
if(requiredNullOptions.isNotEmpty){
locator<PatcherViewModel>().showRequiredOptionDialog();
}
}
}
}
Future showRequireSuggestedAppVersionDialog(
BuildContext context,
String selectedVersion,
String suggestedVersion,
) async {
return showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
title: I18nText('warning'),
content: I18nText(
'appSelectorView.requireSuggestedAppVersionDialogText',
translationParams: {
'suggested': suggestedVersion,
'selected': selectedVersion,
},
child: const Text(
'',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
),
actions: [
CustomMaterialButton(
label: I18nText('okButton'),
onPressed: () => Navigator.of(context).pop(),
),
],
),
);
}
Future showSelectFromStorageDialog(BuildContext context) async {
return showDialog(
context: context,
@ -145,12 +233,10 @@ class AppSelectorViewModel extends BaseViewModel {
),
const SizedBox(height: 30),
CustomMaterialButton(
onPressed: () => selectAppFromStorage(context).then(
(_) {
Navigator.pop(context);
Navigator.pop(context);
},
),
onPressed: () async {
Navigator.pop(context);
await selectAppFromStorage(context);
},
label: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
@ -181,13 +267,14 @@ class AppSelectorViewModel extends BaseViewModel {
Future<void> selectAppFromStorage(BuildContext context) async {
try {
final FilePickerResult? result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['apk'],
final String? result = await FlutterFileDialog.pickFile(
params: const OpenFileDialogParams(
fileExtensionsFilter: ['apk'],
),
);
if (result != null && result.files.single.path != null) {
final File apkFile = File(result.files.single.path!);
final List<String> pathSplit = result.files.single.path!.split('/');
if (result != null) {
final File apkFile = File(result);
final List<String> pathSplit = result.split('/');
pathSplit.removeLast();
final Directory filePickerCacheDir = Directory(pathSplit.join('/'));
final Iterable<File> deletableFiles =
@ -202,17 +289,8 @@ class AppSelectorViewModel extends BaseViewModel {
apkFile.path,
true,
) as ApplicationWithIcon?;
if (application != null) {
locator<PatcherViewModel>().selectedApp = PatchedApplication(
name: application.appName,
packageName: application.packageName,
version: application.versionName!,
apkFilePath: result.files.single.path!,
icon: application.icon,
patchDate: DateTime.now(),
isFromStorage: true,
);
locator<PatcherViewModel>().loadLastSelectedPatches();
if (application != null && context.mounted) {
await selectApp(context, application, true);
}
}
} on Exception catch (e) {

View File

@ -7,7 +7,7 @@ import 'package:revanced_manager/ui/widgets/shared/custom_sliver_app_bar.dart';
import 'package:stacked/stacked.dart';
class ContributorsView extends StatelessWidget {
const ContributorsView({Key? key}) : super(key: key);
const ContributorsView({super.key});
@override
Widget build(BuildContext context) {

View File

@ -9,7 +9,7 @@ import 'package:revanced_manager/ui/widgets/shared/custom_sliver_app_bar.dart';
import 'package:stacked/stacked.dart';
class HomeView extends StatelessWidget {
const HomeView({Key? key}) : super(key: key);
const HomeView({super.key});
@override
Widget build(BuildContext context) {

View File

@ -1,7 +1,7 @@
// ignore_for_file: use_build_context_synchronously
import 'dart:async';
import 'dart:io';
import 'package:cross_connectivity/cross_connectivity.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@ -64,8 +64,9 @@ class HomeViewModel extends BaseViewModel {
flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>()
?.requestPermission();
final bool isConnected = await Connectivity().checkConnection();
?.requestNotificationsPermission();
final bool isConnected = await Connectivity().checkConnectivity() !=
ConnectivityResult.none;
if (!isConnected) {
_toast.showBottom('homeView.noConnection');
}

View File

@ -8,17 +8,18 @@ import 'package:revanced_manager/ui/widgets/shared/custom_sliver_app_bar.dart';
import 'package:stacked/stacked.dart';
class InstallerView extends StatelessWidget {
const InstallerView({Key? key}) : super(key: key);
const InstallerView({super.key});
@override
Widget build(BuildContext context) {
return ViewModelBuilder<InstallerViewModel>.reactive(
onViewModelReady: (model) => model.initialize(context),
viewModelBuilder: () => InstallerViewModel(),
builder: (context, model, child) => WillPopScope(
builder: (context, model, child) => PopScope(
onPopInvoked: (bool didPop) => model.onPopInvoked(context, didPop),
child: SafeArea(
top: false,
bottom: false,
bottom: model.isPatching,
child: Scaffold(
floatingActionButton: Visibility(
visible: !model.isPatching && !model.hasErrors,
@ -83,7 +84,7 @@ class InstallerView extends StatelessWidget {
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
onBackButtonPressed: () => model.onWillPop(context),
onBackButtonPressed: () => model.onBackButtonInvoked(context),
bottom: PreferredSize(
preferredSize: const Size(double.infinity, 1.0),
child: GradientProgressIndicator(progress: model.progress),
@ -111,7 +112,6 @@ class InstallerView extends StatelessWidget {
),
),
),
onWillPop: () => model.onWillPop(context),
),
);
}

View File

@ -18,7 +18,7 @@ import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
import 'package:revanced_manager/utils/about_info.dart';
import 'package:screenshot_callback/screenshot_callback.dart';
import 'package:stacked/stacked.dart';
import 'package:wakelock/wakelock.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
class InstallerViewModel extends BaseViewModel {
final ManagerAPI _managerAPI = locator<ManagerAPI>();
@ -74,7 +74,7 @@ class InstallerViewModel extends BaseViewModel {
screenshotDetected(context);
}
});
await Wakelock.enable();
await WakelockPlus.enable();
await handlePlatformChannelMethods();
await runPatcher();
}
@ -139,7 +139,6 @@ class InstallerViewModel extends BaseViewModel {
}
Future<void> runPatcher() async {
try {
await _patcherAPI.runPatcher(
_app.packageName,
@ -159,7 +158,7 @@ class InstallerViewModel extends BaseViewModel {
// Necessary to reset the state of patches so that they
// can be reloaded again.
_managerAPI.patches.clear();
_managerAPI.patches.clear();
await _patcherAPI.loadPatches();
try {
@ -172,7 +171,7 @@ class InstallerViewModel extends BaseViewModel {
} // ignore
}
}
await Wakelock.disable();
await WakelockPlus.disable();
} on Exception catch (e) {
if (kDebugMode) {
print(e);
@ -182,31 +181,38 @@ class InstallerViewModel extends BaseViewModel {
Future<void> copyLogs() async {
final info = await AboutInfo.getInfo();
dynamic getValue(String patchName, Option option) {
final Option? savedOption =
_managerAPI.getPatchOption(_app.packageName, patchName, option.key);
if (savedOption != null) {
return savedOption.value;
} else {
return option.value;
}
}
final formattedLogs = [
'```',
'~ Device Info',
'- Device Info',
'ReVanced Manager: ${info['version']}',
'Build: ${info['flavor']}',
'Model: ${info['model']}',
'Android version: ${info['androidVersion']}',
'Supported architectures: ${info['supportedArch'].join(", ")}',
'\n~ Patch Info',
'Root permissions: ${isRooted ? 'Yes' : 'No'}',
'\n- Patch Info',
'App: ${_app.packageName} v${_app.version}',
'Patches version: ${_managerAPI.patchesVersion}',
'Patches: ${_patches.map((p) => p.name).toList().join(", ")}',
'\n~ Settings',
'Enabled changing patches: ${_managerAPI.isPatchesChangeEnabled()}',
'Enabled universal patches: ${_managerAPI.areUniversalPatchesEnabled()}',
'Enabled experimental patches: ${_managerAPI.areExperimentalPatchesEnabled()}',
'Patches: ${_patches.map((p) => p.name + (p.options.isEmpty ? '' : ' [${p.options.map((o) => '${o.title}: ${getValue(p.name, o)}').join(", ")}]')).toList().join(", ")}',
'\n- Settings',
'Allow changing patch selection: ${_managerAPI.isPatchesChangeEnabled()}',
'Version compatibility check: ${_managerAPI.isVersionCompatibilityCheckEnabled()}',
'Show universal patches: ${_managerAPI.areUniversalPatchesEnabled()}',
'Patches source: ${_managerAPI.getPatchesRepo()}',
'Integration source: ${_managerAPI.getIntegrationsRepo()}',
'\n~ Logs',
'\n- Logs',
logs,
'```',
];
Clipboard.setData(ClipboardData(text: formattedLogs.join('\n')));
@ -421,25 +427,38 @@ class InstallerViewModel extends BaseViewModel {
}
}
Future<bool> onWillPop(BuildContext context) async {
if (isPatching) {
if (!cancel) {
cancel = true;
_toast.showBottom('installerView.pressBackAgain');
} else if (!isCanceled) {
await stopPatcher();
} else {
_toast.showBottom('installerView.noExit');
}
return false;
}
if (!cancel) {
cleanPatcher();
bool canPop() {
return !isPatching;
}
void onBackButtonInvoked(BuildContext context) {
if (canPop()) {
onPopInvoked(context, true);
} else {
_patcherAPI.cleanPatcher();
onPopInvoked(context, false);
}
}
Future<void> onPopInvoked(BuildContext context, bool didPop) async {
if (didPop) {
if (!cancel) {
cleanPatcher();
} else {
_patcherAPI.cleanPatcher();
}
screenshotCallback.dispose();
Navigator.of(context).pop();
} else {
if (isPatching) {
if (!cancel) {
cancel = true;
_toast.showBottom('installerView.pressBackAgain');
} else if (!isCanceled) {
await stopPatcher();
} else {
_toast.showBottom('installerView.noExit');
}
}
}
screenshotCallback.dispose();
Navigator.of(context).pop();
return true;
}
}

View File

@ -6,20 +6,18 @@ import 'package:revanced_manager/ui/views/navigation/navigation_viewmodel.dart';
import 'package:stacked/stacked.dart';
class NavigationView extends StatelessWidget {
const NavigationView({Key? key}) : super(key: key);
const NavigationView({super.key});
@override
Widget build(BuildContext context) {
return ViewModelBuilder<NavigationViewModel>.reactive(
onViewModelReady: (model) => model.initialize(context),
viewModelBuilder: () => locator<NavigationViewModel>(),
builder: (context, model, child) => WillPopScope(
onWillPop: () async {
if (model.currentIndex == 0) {
return true;
} else {
builder: (context, model, child) => PopScope(
canPop: model.currentIndex == 0,
onPopInvoked: (bool didPop) {
if (!didPop) {
model.setIndex(0);
return false;
}
},
child: Scaffold(

View File

@ -19,7 +19,6 @@ class NavigationViewModel extends IndexTrackingViewModel {
Future<void> initialize(BuildContext context) async {
locator<Toast>().initialize(context);
final SharedPreferences prefs = await SharedPreferences.getInstance();
await requestManageExternalStorage();
if (prefs.getBool('permissionsRequested') == null) {
await Permission.storage.request();
@ -60,17 +59,6 @@ class NavigationViewModel extends IndexTrackingViewModel {
);
}
Future<void> requestManageExternalStorage() async {
final manageExternalStorageStatus =
await Permission.manageExternalStorage.status;
if (manageExternalStorageStatus.isDenied) {
await Permission.manageExternalStorage.request();
}
if (manageExternalStorageStatus.isPermanentlyDenied) {
await openAppSettings();
}
}
Widget getViewForIndex(int index) {
switch (index) {
case 0:

View File

@ -48,6 +48,10 @@ class PatchOptionsView extends StatelessWidget {
icon: const Icon(
Icons.history,
),
tooltip: FlutterI18n.translate(
context,
'patchOptionsView.resetOptionsTooltip',
),
),
],
),
@ -57,8 +61,8 @@ class PatchOptionsView extends StatelessWidget {
child: Column(
children: [
for (final Option option in model.visibleOptions)
if (option.optionClassType == 'StringPatchOption' ||
option.optionClassType == 'IntPatchOption')
if (option.valueType == 'String' ||
option.valueType == 'Int')
IntAndStringPatchOption(
patchOption: option,
removeOption: (option) {
@ -68,7 +72,7 @@ class PatchOptionsView extends StatelessWidget {
model.modifyOptions(value, option);
},
)
else if (option.optionClassType == 'BooleanPatchOption')
else if (option.valueType == 'Boolean')
BooleanPatchOption(
patchOption: option,
removeOption: (option) {
@ -78,10 +82,10 @@ class PatchOptionsView extends StatelessWidget {
model.modifyOptions(value, option);
},
)
else if (option.optionClassType ==
'StringListPatchOption' ||
option.optionClassType == 'IntListPatchOption' ||
option.optionClassType == 'LongListPatchOption')
else if (option.valueType ==
'StringArray' ||
option.valueType == 'IntArray' ||
option.valueType == 'LongArray')
IntStringLongListPatchOption(
patchOption: option,
removeOption: (option) {

View File

@ -38,11 +38,11 @@ class PatchOptionsViewModel extends BaseViewModel {
option.required &&
!savedOptions.any((sOption) => sOption.key == option.key),
)
.toList(),
,
];
} else {
visibleOptions = [
...options.where((option) => option.required).toList(),
...options.where((option) => option.required),
];
}
}
@ -62,7 +62,10 @@ class PatchOptionsViewModel extends BaseViewModel {
for (final Option option in options) {
if (!visibleOptions.any((vOption) => vOption.key == option.key)) {
_managerAPI.clearPatchOption(
selectedApp, _managerAPI.selectedPatch!.name, option.key);
selectedApp,
_managerAPI.selectedPatch!.name,
option.key,
);
}
}
for (final Option option in visibleOptions) {
@ -70,7 +73,10 @@ class PatchOptionsViewModel extends BaseViewModel {
requiredNullOptions.add(option);
} else {
_managerAPI.setPatchOption(
option, _managerAPI.selectedPatch!.name, selectedApp);
option,
_managerAPI.selectedPatch!.name,
selectedApp,
);
}
}
if (requiredNullOptions.isNotEmpty) {
@ -89,7 +95,8 @@ class PatchOptionsViewModel extends BaseViewModel {
final Option modifiedOption = Option(
title: option.title,
description: option.description,
optionClassType: option.optionClassType,
values: option.values,
valueType: option.valueType,
value: value,
required: option.required,
key: option.key,
@ -107,7 +114,8 @@ class PatchOptionsViewModel extends BaseViewModel {
final Option defaultOption = Option(
title: option.title,
description: option.description,
optionClassType: option.optionClassType,
values: option.values,
valueType: option.valueType,
value: option.value is List ? option.value.toList() : option.value,
required: option.required,
key: option.key,
@ -147,7 +155,7 @@ class PatchOptionsViewModel extends BaseViewModel {
),
actions: [
CustomMaterialButton(
label: I18nText('okButton'),
label: I18nText('cancelButton'),
onPressed: () {
Navigator.of(context).pop();
},
@ -165,30 +173,36 @@ class PatchOptionsViewModel extends BaseViewModel {
.map((e) {
return CustomCard(
padding: const EdgeInsets.all(4),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
backgroundColor: Theme.of(context).colorScheme.surface,
onTap: () {
addOption(e);
Navigator.pop(context);
},
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
child: Row(
children: [
Text(
e.title,
style: const TextStyle(
fontSize: 16,
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
e.title,
style: const TextStyle(
fontSize: 16,
),
),
const SizedBox(height: 4),
Text(
e.description,
style: TextStyle(
fontSize: 14,
color: Theme.of(context).colorScheme.onSurface,
),
),
],
),
),
const SizedBox(height: 4),
Text(
e.description,
style: TextStyle(
fontSize: 14,
color: Theme.of(context).colorScheme.onSecondaryContainer,
),
)
],
),
),
@ -229,7 +243,10 @@ Future<void> showRequiredOptionNullDialog(
locator<PatcherViewModel>().notifyListeners();
for (final option in options) {
managerAPI.clearPatchOption(
selectedApp, managerAPI.selectedPatch!.name, option.key);
selectedApp,
managerAPI.selectedPatch!.name,
option.key,
);
}
Navigator.of(context)
..pop()

View File

@ -9,7 +9,7 @@ import 'package:revanced_manager/ui/widgets/shared/custom_sliver_app_bar.dart';
import 'package:stacked/stacked.dart';
class PatcherView extends StatelessWidget {
const PatcherView({Key? key}) : super(key: key);
const PatcherView({super.key});
@override
Widget build(BuildContext context) {

View File

@ -1,6 +1,10 @@
// ignore_for_file: use_build_context_synchronously
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:injectable/injectable.dart';
import 'package:revanced_manager/app/app.locator.dart';
@ -80,7 +84,8 @@ class PatcherViewModel extends BaseViewModel {
}
bool checkRequiredPatchOption(BuildContext context) {
if (getNullRequiredOptions(selectedPatches, selectedApp!.packageName).isNotEmpty) {
if (getNullRequiredOptions(selectedPatches, selectedApp!.packageName)
.isNotEmpty) {
showRequiredOptionDialog(context);
return false;
}
@ -156,6 +161,29 @@ class PatcherViewModel extends BaseViewModel {
return text;
}
String getCurrentVersionString(BuildContext context) {
return '${FlutterI18n.translate(
context,
'appSelectorCard.currentVersion',
)}: v${selectedApp!.version}';
}
Future<void> searchSuggestedVersionOnWeb() async {
final String suggestedVersion =
_patcherAPI.getSuggestedVersion(selectedApp!.packageName);
if (suggestedVersion.isNotEmpty) {
await openDefaultBrowser(
'${selectedApp!.packageName} apk version v$suggestedVersion');
} else {
await openDefaultBrowser('${selectedApp!.packageName} apk');
}
}
String getSuggestedVersion() {
return _patcherAPI.getSuggestedVersion(selectedApp!.packageName);
}
String getSuggestedVersionString(BuildContext context) {
String suggestedVersion =
_patcherAPI.getSuggestedVersion(selectedApp!.packageName);
@ -168,14 +196,26 @@ class PatcherViewModel extends BaseViewModel {
suggestedVersion = 'v$suggestedVersion';
}
return '${FlutterI18n.translate(
context,
'appSelectorCard.currentVersion',
)}: v${selectedApp!.version}\n${FlutterI18n.translate(
context,
'appSelectorCard.suggestedVersion',
)}: $suggestedVersion';
}
Future<void> openDefaultBrowser(String query) async {
if (Platform.isAndroid) {
try {
const platform = MethodChannel('app.revanced.manager.flutter/browser');
await platform.invokeMethod('openBrowser', {'query': query});
} catch (e) {
if (kDebugMode) {
print(e);
}
}
} else {
throw 'Platform not supported';
}
}
Future<void> loadLastSelectedPatches() async {
this.selectedPatches.clear();
removedPatches.clear();
@ -190,7 +230,7 @@ class PatcherViewModel extends BaseViewModel {
this.selectedPatches.clear();
this.selectedPatches.addAll(patches.where((patch) => !patch.excluded));
}
if (!_managerAPI.areExperimentalPatchesEnabled()) {
if (_managerAPI.isVersionCompatibilityCheckEnabled()) {
this.selectedPatches.removeWhere((patch) => !isPatchSupported(patch));
}
if (!_managerAPI.areUniversalPatchesEnabled()) {
@ -199,11 +239,12 @@ class PatcherViewModel extends BaseViewModel {
.removeWhere((patch) => patch.compatiblePackages.isEmpty);
}
final usedPatches = _managerAPI.getUsedPatches(selectedApp!.packageName);
for (final patch in usedPatches){
if (!patches.any((p) => p.name == patch.name)){
for (final patch in usedPatches) {
if (!patches.any((p) => p.name == patch.name)) {
removedPatches.add('${patch.name}');
for (final option in patch.options) {
_managerAPI.clearPatchOption(selectedApp!.packageName, patch.name, option.key);
_managerAPI.clearPatchOption(
selectedApp!.packageName, patch.name, option.key);
}
}
}

View File

@ -3,14 +3,11 @@ import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/services/manager_api.dart';
import 'package:revanced_manager/ui/views/patches_selector/patches_selector_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/patchesSelectorView/patch_item.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_popup_menu.dart';
import 'package:revanced_manager/ui/widgets/shared/search_bar.dart';
import 'package:revanced_manager/utils/check_for_supported_patch.dart';
import 'package:stacked/stacked.dart';
class PatchesSelectorView extends StatefulWidget {
const PatchesSelectorView({Key? key}) : super(key: key);
const PatchesSelectorView({super.key});
@override
State<PatchesSelectorView> createState() => _PatchesSelectorViewState();
@ -62,12 +59,10 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
floating: true,
title: I18nText(
'patchesSelectorView.viewTitle',
child: Text(
'',
style: TextStyle(
color: Theme.of(context).textTheme.titleLarge!.color,
),
),
),
titleTextStyle: TextStyle(
fontSize: 22.0,
color: Theme.of(context).textTheme.titleLarge!.color,
),
leading: IconButton(
icon: Icon(
@ -80,41 +75,34 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
},
),
actions: [
FittedBox(
fit: BoxFit.scaleDown,
child: Container(
margin: const EdgeInsets.only(top: 12, bottom: 12),
padding:
const EdgeInsets.symmetric(horizontal: 6, vertical: 6),
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.tertiary
.withOpacity(0.5),
borderRadius: BorderRadius.circular(6),
),
child: Text(
model.patchesVersion!,
style: TextStyle(
color: Theme.of(context).textTheme.titleLarge!.color,
),
Container(
margin: const EdgeInsets.symmetric(vertical: 12),
padding: const EdgeInsets.symmetric(horizontal: 6),
decoration: BoxDecoration(
color:
Theme.of(context).colorScheme.tertiary.withOpacity(0.5),
borderRadius: BorderRadius.circular(6),
),
alignment: Alignment.center,
child: Text(
model.patchesVersion!,
style: TextStyle(
color: Theme.of(context).textTheme.titleLarge!.color,
),
),
),
CustomPopupMenu(
onSelected: (value) =>
{model.onMenuSelection(value, context)},
children: {
0: I18nText(
'patchesSelectorView.loadPatchesSelection',
child: const Text(
'',
style: TextStyle(
fontWeight: FontWeight.bold,
),
PopupMenuButton(
onSelected: (value) {
model.onMenuSelection(value, context);
},
itemBuilder: (BuildContext context) => <PopupMenuEntry>[
PopupMenuItem(
value: 0,
child: I18nText(
'patchesSelectorView.loadPatchesSelection',
),
),
},
],
),
],
bottom: PreferredSize(
@ -192,185 +180,39 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
),
],
),
if (model.newPatchExists())
if (model.getQueriedPatches(_query).any((patch) => model.isPatchNew(patch)))
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(
vertical: 10.0,
),
child: Container(
padding: const EdgeInsets.only(
top: 10.0,
bottom: 10.0,
left: 5.0,
),
child: I18nText(
'patchesSelectorView.newPatches',
child: Text(
'',
style: TextStyle(
color: Theme.of(context)
.colorScheme
.primary,
),
),
),
),
),
model.getPatchCategory(context, 'patchesSelectorView.newPatches'),
...model.getQueriedPatches(_query).map((patch) {
if (model.isPatchNew(patch)) {
return PatchItem(
name: patch.name,
simpleName: patch.getSimpleName(),
description: patch.description ?? '',
packageVersion:
model.getAppInfo().version,
supportedPackageVersions:
model.getSupportedVersions(patch),
isUnsupported: !isPatchSupported(patch),
isChangeEnabled:
_managerAPI.isPatchesChangeEnabled(),
hasUnsupportedPatchOption:
hasUnsupportedRequiredOption(
patch.options,
patch,
),
options: patch.options,
isSelected: model.isSelected(patch),
navigateToOptions: (options) =>
model.navigateToPatchOptions(
options,
patch,
),
onChanged: (value) => model.selectPatch(
patch,
value,
context,
),
);
return model.getPatchItem(context, patch);
} else {
return Container();
}
}),
Padding(
padding: const EdgeInsets.symmetric(
vertical: 10.0,
),
child: Container(
padding: const EdgeInsets.only(
top: 10.0,
bottom: 10.0,
left: 5.0,
),
child: I18nText(
'patchesSelectorView.patches',
child: Text(
'',
style: TextStyle(
color: Theme.of(context)
.colorScheme
.primary,
),
),
),
),
),
if (model.getQueriedPatches(_query).any((patch) => !model.isPatchNew(patch) && patch.compatiblePackages.isNotEmpty))
model.getPatchCategory(context, 'patchesSelectorView.patches'),
],
),
...model.getQueriedPatches(_query).map(
(patch) {
if (patch.compatiblePackages.isNotEmpty) {
return PatchItem(
name: patch.name,
simpleName: patch.getSimpleName(),
description: patch.description ?? '',
packageVersion: model.getAppInfo().version,
supportedPackageVersions:
model.getSupportedVersions(patch),
isUnsupported: !isPatchSupported(patch),
isChangeEnabled:
_managerAPI.isPatchesChangeEnabled(),
hasUnsupportedPatchOption:
hasUnsupportedRequiredOption(
patch.options, patch),
options: patch.options,
isSelected: model.isSelected(patch),
navigateToOptions: (options) =>
model.navigateToPatchOptions(
options,
patch,
),
onChanged: (value) => model.selectPatch(
patch,
value,
context,
),
);
if (patch.compatiblePackages.isNotEmpty && !model.isPatchNew(patch)) {
return model.getPatchItem(context, patch);
} else {
return Container();
}
},
),
if (_managerAPI.areUniversalPatchesEnabled())
if (model.getQueriedPatches(_query).any((patch) => patch.compatiblePackages.isEmpty))
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(
vertical: 10.0,
),
child: Container(
padding: const EdgeInsets.only(
top: 10.0,
bottom: 10.0,
left: 5.0,
),
child: I18nText(
'patchesSelectorView.universalPatches',
child: Text(
'',
style: TextStyle(
color: Theme.of(context)
.colorScheme
.primary,
),
),
),
),
),
model.getPatchCategory(context, 'patchesSelectorView.universalPatches'),
...model.getQueriedPatches(_query).map((patch) {
if (patch.compatiblePackages.isEmpty) {
return PatchItem(
name: patch.name,
simpleName: patch.getSimpleName(),
description: patch.description ?? '',
packageVersion:
model.getAppInfo().version,
supportedPackageVersions:
model.getSupportedVersions(patch),
isUnsupported: !isPatchSupported(patch),
isChangeEnabled:
_managerAPI.isPatchesChangeEnabled(),
hasUnsupportedPatchOption:
hasUnsupportedRequiredOption(
patch.options,
patch,
),
options: patch.options,
isSelected: model.isSelected(patch),
navigateToOptions: (options) =>
model.navigateToPatchOptions(
options,
patch,
),
onChanged: (value) => model.selectPatch(
patch,
value,
context,
),
);
if (patch.compatiblePackages.isEmpty && !model.isPatchNew(patch)) {
return model.getPatchItem(context, patch);
} else {
return Container();
}

View File

@ -9,6 +9,7 @@ import 'package:revanced_manager/services/manager_api.dart';
import 'package:revanced_manager/services/patcher_api.dart';
import 'package:revanced_manager/services/toast.dart';
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/patchesSelectorView/patch_item.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
import 'package:revanced_manager/utils/check_for_supported_patch.dart';
import 'package:stacked/stacked.dart';
@ -169,7 +170,7 @@ class PatchesSelectorViewModel extends BaseViewModel {
.where(
(element) =>
!element.excluded &&
(_managerAPI.areExperimentalPatchesEnabled() ||
(!_managerAPI.isVersionCompatibilityCheckEnabled() ||
isPatchSupported(element)),
),
);
@ -209,7 +210,10 @@ class PatchesSelectorViewModel extends BaseViewModel {
query.isEmpty ||
query.length < 2 ||
patch.name.toLowerCase().contains(query.toLowerCase()) ||
patch.getSimpleName().toLowerCase().contains(query.toLowerCase()),
patch.name
.replaceAll(RegExp(r'[^\w\s]+'), '')
.toLowerCase()
.contains(query.toLowerCase()),
)
.toList();
if (_managerAPI.areUniversalPatchesEnabled()) {
@ -221,6 +225,57 @@ class PatchesSelectorViewModel extends BaseViewModel {
}
}
Widget getPatchItem(BuildContext context, Patch patch) {
return PatchItem(
name: patch.name,
simpleName: patch.getSimpleName(),
description: patch.description ?? '',
packageVersion: getAppInfo().version,
supportedPackageVersions: getSupportedVersions(patch),
isUnsupported: !isPatchSupported(patch),
isChangeEnabled: _managerAPI.isPatchesChangeEnabled(),
hasUnsupportedPatchOption: hasUnsupportedRequiredOption(
patch.options,
patch,
),
options: patch.options,
isSelected: isSelected(patch),
navigateToOptions: (options) => navigateToPatchOptions(
options,
patch,
),
onChanged: (value) => selectPatch(
patch,
value,
context,
),
);
}
Widget getPatchCategory(BuildContext context, String category) {
return Padding(
padding: const EdgeInsets.symmetric(
vertical: 10.0,
),
child: Container(
padding: const EdgeInsets.only(
top: 10.0,
bottom: 10.0,
left: 5.0,
),
child: I18nText(
category,
child: Text(
'',
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
),
),
),
),
);
}
PatchedApplication getAppInfo() {
return locator<PatcherViewModel>().selectedApp!;
}
@ -236,12 +291,6 @@ class PatchesSelectorViewModel extends BaseViewModel {
}
}
bool newPatchExists() {
return patches.any(
(patch) => isPatchNew(patch),
);
}
List<String> getSupportedVersions(Patch patch) {
final PatchedApplication app = locator<PatcherViewModel>().selectedApp!;
final Package? package = patch.compatiblePackages.firstWhereOrNull(
@ -281,7 +330,7 @@ class PatchesSelectorViewModel extends BaseViewModel {
this.selectedPatches.addAll(
patches.where((patch) => selectedPatches.contains(patch.name)),
);
if (!_managerAPI.areExperimentalPatchesEnabled()) {
if (_managerAPI.isVersionCompatibilityCheckEnabled()) {
this.selectedPatches.removeWhere((patch) => !isPatchSupported(patch));
}
} else {

View File

@ -67,6 +67,7 @@ class SManageApiUrl extends BaseViewModel {
apiUrl = 'https://$apiUrl';
}
_managerAPI.setApiUrl(apiUrl);
_toast.showBottom('settingsView.restartAppForChanges');
Navigator.of(context).pop();
},
),

View File

@ -6,46 +6,98 @@ import 'package:flutter/services.dart';
import 'package:flutter_i18n/widgets/I18nText.dart';
import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/services/manager_api.dart';
import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_section.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
import 'package:stacked/stacked.dart';
final _settingViewModel = SettingsViewModel();
class SUpdateThemeUI extends StatefulWidget {
const SUpdateThemeUI({super.key});
// ignore: constant_identifier_names
const int ANDROID_12_SDK_VERSION = 31;
@override
State<SUpdateThemeUI> createState() => _SUpdateThemeUIState();
}
class SUpdateTheme extends BaseViewModel {
final ManagerAPI _managerAPI = locator<ManagerAPI>();
class _SUpdateThemeUIState extends State<SUpdateThemeUI> {
final ManagerAPI managerAPI = locator<ManagerAPI>();
@override
Widget build(BuildContext context) {
return SettingsSection(
title: 'settingsView.appearanceSectionTitle',
children: <Widget>[
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText(
'settingsView.themeModeLabel',
child: const Text(
'',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
),
),
),
trailing: CustomMaterialButton(
label: getThemeModeName(),
onPressed: () => {showThemeDialog(context)},
),
onTap: () => {showThemeDialog(context)},
),
if (managerAPI.isDynamicThemeAvailable)
SwitchListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText(
'settingsView.dynamicThemeLabel',
child: const Text(
'',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
),
),
),
subtitle: I18nText('settingsView.dynamicThemeHint'),
value: getDynamicThemeStatus(),
onChanged: (value) => {
setUseDynamicTheme(
context,
value,
),
},
),
],
);
}
bool getDynamicThemeStatus() {
return _managerAPI.getUseDynamicTheme();
return managerAPI.getUseDynamicTheme();
}
Future<void> setUseDynamicTheme(BuildContext context, bool value) async {
await _managerAPI.setUseDynamicTheme(value);
await managerAPI.setUseDynamicTheme(value);
final int currentTheme = (DynamicTheme.of(context)!.themeId ~/ 2) * 2;
await DynamicTheme.of(context)!.setTheme(currentTheme + (value ? 1 : 0));
notifyListeners();
setState(() {});
}
int getThemeMode() {
return _managerAPI.getThemeMode();
return managerAPI.getThemeMode();
}
Future<void> setThemeMode(BuildContext context, int value) async {
await _managerAPI.setThemeMode(value);
await managerAPI.setThemeMode(value);
final bool isDynamicTheme = DynamicTheme.of(context)!.themeId.isEven;
await DynamicTheme.of(context)!.setTheme(value * 2 + (isDynamicTheme ? 0 : 1));
final bool isLight = value != 2 && (value == 1 || DynamicTheme.of(context)!.theme.brightness == Brightness.light);
await DynamicTheme.of(context)!
.setTheme(value * 2 + (isDynamicTheme ? 0 : 1));
final bool isLight = value != 2 &&
(value == 1 ||
DynamicTheme.of(context)!.theme.brightness == Brightness.light);
SystemChrome.setSystemUIOverlayStyle(
SystemUiOverlayStyle(
systemNavigationBarIconBrightness:
isLight ? Brightness.dark : Brightness.light,
isLight ? Brightness.dark : Brightness.light,
),
);
notifyListeners();
setState(() {});
}
I18nText getThemeModeName() {
@ -131,63 +183,3 @@ class SUpdateTheme extends BaseViewModel {
);
}
}
final sUpdateTheme = SUpdateTheme();
class SUpdateThemeUI extends StatelessWidget {
const SUpdateThemeUI({super.key});
@override
Widget build(BuildContext context) {
return SettingsSection(
title: 'settingsView.appearanceSectionTitle',
children: <Widget>[
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText(
'settingsView.themeModeLabel',
child: const Text(
'',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
),
),
),
trailing: CustomMaterialButton(
label: sUpdateTheme.getThemeModeName(),
onPressed: () => { sUpdateTheme.showThemeDialog(context) },
),
onTap: () => { sUpdateTheme.showThemeDialog(context) },
),
FutureBuilder<int>(
future: _settingViewModel.getSdkVersion(),
builder: (context, snapshot) => Visibility(
visible:
snapshot.hasData && snapshot.data! >= ANDROID_12_SDK_VERSION,
child: SwitchListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText(
'settingsView.dynamicThemeLabel',
child: const Text(
'',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
),
),
),
subtitle: I18nText('settingsView.dynamicThemeHint'),
value: _settingViewModel.sUpdateTheme.getDynamicThemeStatus(),
onChanged: (value) => {
_settingViewModel.sUpdateTheme.setUseDynamicTheme(
context,
value,
),
},
),
),
),
],
);
}
}

View File

@ -6,14 +6,14 @@ import 'package:google_fonts/google_fonts.dart';
import 'package:revanced_manager/ui/views/settings/settingsFragment/settings_update_theme.dart';
import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_advanced_section.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_debug_section.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_export_section.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_info_section.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_team_section.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_sliver_app_bar.dart';
import 'package:stacked/stacked.dart';
class SettingsView extends StatelessWidget {
const SettingsView({Key? key}) : super(key: key);
const SettingsView({super.key});
static const _settingsDivider =
Divider(thickness: 1.0, indent: 20.0, endIndent: 20.0);
@ -40,16 +40,24 @@ class SettingsView extends StatelessWidget {
SliverList(
delegate: SliverChildListDelegate.fixed(
<Widget>[
SUpdateThemeUI(),
// SUpdateLanguageUI(),
// _settingsDivider,
STeamSection(),
_settingsDivider,
SAdvancedSection(),
_settingsDivider,
SExportSection(),
_settingsDivider,
SInfoSection(),
ListView(
padding: EdgeInsets.zero,
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
children: const [
SUpdateThemeUI(),
// _settingsDivider,
// SUpdateLanguageUI(),
_settingsDivider,
SAdvancedSection(),
_settingsDivider,
SExportSection(),
_settingsDivider,
STeamSection(),
_settingsDivider,
SDebugSection(),
],
),
],
),
),

View File

@ -1,9 +1,7 @@
import 'dart:io';
import 'package:cr_file_saver/file_saver.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_file_dialog/flutter_file_dialog.dart';
import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:logcat/logcat.dart';
import 'package:path_provider/path_provider.dart';
@ -14,9 +12,8 @@ import 'package:revanced_manager/services/toast.dart';
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
import 'package:revanced_manager/ui/views/patches_selector/patches_selector_viewmodel.dart';
import 'package:revanced_manager/ui/views/settings/settingsFragment/settings_update_language.dart';
import 'package:revanced_manager/ui/views/settings/settingsFragment/settings_update_theme.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
import 'package:share_extend/share_extend.dart';
import 'package:share_plus/share_plus.dart';
import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart';
@ -29,7 +26,6 @@ class SettingsViewModel extends BaseViewModel {
final Toast _toast = locator<Toast>();
final SUpdateLanguage sUpdateLanguage = SUpdateLanguage();
final SUpdateTheme sUpdateTheme = SUpdateTheme();
void navigateToContributors() {
_navigationService.navigateTo(Routes.contributorsView);
@ -135,15 +131,66 @@ class SettingsViewModel extends BaseViewModel {
notifyListeners();
}
bool areExperimentalPatchesEnabled() {
return _managerAPI.areExperimentalPatchesEnabled();
bool isVersionCompatibilityCheckEnabled() {
return _managerAPI.isVersionCompatibilityCheckEnabled();
}
void useExperimentalPatches(bool value) {
_managerAPI.enableExperimentalPatchesStatus(value);
void useVersionCompatibilityCheck(bool value) {
_managerAPI.enableVersionCompatibilityCheckStatus(value);
notifyListeners();
}
bool isRequireSuggestedAppVersionEnabled() {
return _managerAPI.isRequireSuggestedAppVersionEnabled();
}
Future<void>? showRequireSuggestedAppVersionDialog(
BuildContext context, bool value,) {
if (!value) {
return showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
title: I18nText('warning'),
content: I18nText(
'settingsView.requireSuggestedAppVersionDialogText',
child: const Text(
'',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
),
actions: [
CustomMaterialButton(
isFilled: false,
label: I18nText('yesButton'),
onPressed: () {
_managerAPI.enableRequireSuggestedAppVersionStatus(false);
Navigator.of(context).pop();
},
),
CustomMaterialButton(
label: I18nText('noButton'),
onPressed: () {
Navigator.of(context).pop();
},
),
],
),
);
} else {
_managerAPI.enableRequireSuggestedAppVersionStatus(true);
if (!_managerAPI.suggestedAppVersionSelected) {
_patcherViewModel.selectedApp = null;
}
return null;
}
}
void deleteKeystore() {
_managerAPI.deleteKeystore();
_toast.showBottom('settingsView.regeneratedKeystore');
@ -162,10 +209,10 @@ class SettingsViewModel extends BaseViewModel {
if (outFile.existsSync()) {
final String dateTime =
DateTime.now().toString().replaceAll(' ', '_').split('.').first;
await CRFileSaver.saveFileWithDialog(
SaveFileDialogParams(
await FlutterFileDialog.saveFile(
params: SaveFileDialogParams(
sourceFilePath: outFile.path,
destinationFileName: 'selected_patches_$dateTime.json',
fileName: 'selected_patches_$dateTime.json',
),
);
_toast.showBottom('settingsView.exportedPatches');
@ -182,12 +229,13 @@ class SettingsViewModel extends BaseViewModel {
Future<void> importPatches(BuildContext context) async {
if (isPatchesChangeEnabled()) {
try {
final FilePickerResult? result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['json'],
final String? result = await FlutterFileDialog.pickFile(
params: const OpenFileDialogParams(
fileExtensionsFilter: ['json'],
),
);
if (result != null && result.files.single.path != null) {
final File inFile = File(result.files.single.path!);
if (result != null) {
final File inFile = File(result);
inFile.copySync(_managerAPI.storedPatchesFile);
inFile.delete();
if (_patcherViewModel.selectedApp != null) {
@ -212,10 +260,10 @@ class SettingsViewModel extends BaseViewModel {
if (outFile.existsSync()) {
final String dateTime =
DateTime.now().toString().replaceAll(' ', '_').split('.').first;
await CRFileSaver.saveFileWithDialog(
SaveFileDialogParams(
await FlutterFileDialog.saveFile(
params: SaveFileDialogParams(
sourceFilePath: outFile.path,
destinationFileName: 'keystore_$dateTime.keystore',
fileName: 'keystore_$dateTime.keystore',
),
);
_toast.showBottom('settingsView.exportedKeystore');
@ -231,9 +279,9 @@ class SettingsViewModel extends BaseViewModel {
Future<void> importKeystore() async {
try {
final FilePickerResult? result = await FilePicker.platform.pickFiles();
if (result != null && result.files.single.path != null) {
final File inFile = File(result.files.single.path!);
final String? result = await FlutterFileDialog.pickFile();
if (result != null) {
final File inFile = File(result);
inFile.copySync(_managerAPI.keystoreFile);
_toast.showBottom('settingsView.importedKeystore');
@ -256,11 +304,6 @@ class SettingsViewModel extends BaseViewModel {
_toast.showBottom('settingsView.resetStoredPatches');
}
Future<int> getSdkVersion() async {
final AndroidDeviceInfo info = await DeviceInfoPlugin().androidInfo;
return info.version.sdkInt;
}
Future<void> deleteLogs() async {
final Directory appCacheDir = await getTemporaryDirectory();
final Directory logsDir = Directory('${appCacheDir.path}/logs');
@ -284,6 +327,6 @@ class SettingsViewModel extends BaseViewModel {
File('${logDir.path}/revanced-manager_logcat_$dateTime.log');
final String logs = await Logcat.execute();
logcat.writeAsStringSync(logs);
ShareExtend.share(logcat.path, 'file');
await Share.shareXFiles([XFile(logcat.path)]);
}
}

View File

@ -9,9 +9,9 @@ import 'package:stacked/stacked.dart';
class AppInfoView extends StatelessWidget {
const AppInfoView({
Key? key,
super.key,
required this.app,
}) : super(key: key);
});
final PatchedApplication app;
@override

View File

@ -3,7 +3,7 @@ import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
import 'package:skeletons/skeletons.dart';
class AppSkeletonLoader extends StatelessWidget {
const AppSkeletonLoader({Key? key}) : super(key: key);
const AppSkeletonLoader({super.key});
@override
Widget build(BuildContext context) {

View File

@ -5,7 +5,7 @@ import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
class InstalledAppItem extends StatefulWidget {
const InstalledAppItem({
Key? key,
super.key,
required this.name,
required this.pkgName,
required this.icon,
@ -13,7 +13,9 @@ class InstalledAppItem extends StatefulWidget {
required this.suggestedVersion,
required this.installedVersion,
this.onTap,
}) : super(key: key);
this.onLinkTap,
});
final String name;
final String pkgName;
final Uint8List icon;
@ -21,6 +23,7 @@ class InstalledAppItem extends StatefulWidget {
final String suggestedVersion;
final String installedVersion;
final Function()? onTap;
final Function()? onLinkTap;
@override
State<InstalledAppItem> createState() => _InstalledAppItemState();
@ -71,17 +74,52 @@ class _InstalledAppItemState extends State<InstalledAppItem> {
),
),
Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
children: [
I18nText(
'suggested',
translationParams: {
'version': widget.suggestedVersion.isEmpty
? FlutterI18n.translate(
context,
'appSelectorCard.allVersions',
)
: 'v${widget.suggestedVersion}',
},
Material(
color: Theme.of(context).colorScheme.secondaryContainer,
borderRadius:
const BorderRadius.all(Radius.circular(8)),
child: InkWell(
onTap: widget.onLinkTap,
borderRadius:
const BorderRadius.all(Radius.circular(8)),
child: Container(
padding: const EdgeInsets.all(4),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
I18nText(
'suggested',
translationParams: {
'version': widget.suggestedVersion.isEmpty
? FlutterI18n.translate(
context,
'appSelectorCard.allVersions',
)
: 'v${widget.suggestedVersion}',
},
child: Text(
'',
style: TextStyle(
color: Theme.of(context)
.colorScheme
.onSecondaryContainer,
),
),
),
const SizedBox(width: 4),
Icon(
Icons.search,
size: 16,
color: Theme.of(context)
.colorScheme
.onSecondaryContainer,
),
],
),
),
),
),
const SizedBox(width: 4),
Text(

View File

@ -4,16 +4,19 @@ import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
class NotInstalledAppItem extends StatefulWidget {
const NotInstalledAppItem({
Key? key,
super.key,
required this.name,
required this.patchesCount,
required this.suggestedVersion,
this.onTap,
}) : super(key: key);
this.onLinkTap,
});
final String name;
final int patchesCount;
final String suggestedVersion;
final Function()? onTap;
final Function()? onLinkTap;
@override
State<NotInstalledAppItem> createState() => _NotInstalledAppItem();
@ -65,17 +68,52 @@ class _NotInstalledAppItem extends State<NotInstalledAppItem> {
),
),
Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
children: [
I18nText(
'suggested',
translationParams: {
'version': widget.suggestedVersion.isEmpty
? FlutterI18n.translate(
context,
'appSelectorCard.allVersions',
)
: 'v${widget.suggestedVersion}',
},
Material(
color: Theme.of(context).colorScheme.secondaryContainer,
borderRadius:
const BorderRadius.all(Radius.circular(8)),
child: InkWell(
onTap: widget.onLinkTap,
borderRadius:
const BorderRadius.all(Radius.circular(8)),
child: Container(
padding: const EdgeInsets.all(4),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
I18nText(
'suggested',
translationParams: {
'version': widget.suggestedVersion.isEmpty
? FlutterI18n.translate(
context,
'appSelectorCard.allVersions',
)
: 'v${widget.suggestedVersion}',
},
child: Text(
'',
style: TextStyle(
color: Theme.of(context)
.colorScheme
.onSecondaryContainer,
),
),
),
const SizedBox(width: 4),
Icon(
Icons.search,
size: 16,
color: Theme.of(context)
.colorScheme
.onSecondaryContainer,
),
],
),
),
),
),
const SizedBox(width: 4),
Text(

View File

@ -1,16 +1,16 @@
import 'package:flutter/material.dart';
import 'package:flutter_cache_manager/file.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:revanced_manager/services/download_manager.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
import 'package:url_launcher/url_launcher.dart';
class ContributorsCard extends StatefulWidget {
const ContributorsCard({
Key? key,
super.key,
required this.title,
required this.contributors,
}) : super(key: key);
});
final String title;
final List<dynamic> contributors;
@ -58,7 +58,7 @@ class _ContributorsCardState extends State<ContributorsCard> {
mode: LaunchMode.externalApplication,
),
child: FutureBuilder<File?>(
future: DefaultCacheManager().getSingleFile(
future: DownloadManager().getSingleFile(
widget.contributors[index]['avatar_url'],
),
builder: (context, snapshot) => snapshot.hasData

View File

@ -10,7 +10,7 @@ import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
//ignore: must_be_immutable
class InstalledAppsCard extends StatelessWidget {
InstalledAppsCard({Key? key}) : super(key: key);
InstalledAppsCard({super.key});
List<PatchedApplication> apps = locator<HomeViewModel>().patchedInstalledApps;
final ManagerAPI _managerAPI = locator<ManagerAPI>();

View File

@ -7,10 +7,10 @@ import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
class LatestCommitCard extends StatefulWidget {
const LatestCommitCard({
Key? key,
super.key,
required this.model,
required this.parentContext,
}) : super(key: key);
});
final HomeViewModel model;
final BuildContext parentContext;
@ -75,7 +75,7 @@ class _LatestCommitCardState extends State<LatestCommitCard> {
const SizedBox(height: 16),
// ReVanced Patches
// Patches
CustomCard(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
@ -84,7 +84,7 @@ class _LatestCommitCardState extends State<LatestCommitCard> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
const Text('ReVanced Patches'),
const Text('Patches'),
const SizedBox(height: 4),
Row(
children: <Widget>[

View File

@ -7,9 +7,10 @@ import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
class AppSelectorCard extends StatelessWidget {
const AppSelectorCard({
Key? key,
super.key,
required this.onPressed,
}) : super(key: key);
});
final Function() onPressed;
@override
@ -61,11 +62,52 @@ class AppSelectorCard extends StatelessWidget {
Container()
else
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(height: 4),
Text(
locator<PatcherViewModel>()
.getSuggestedVersionString(context),
locator<PatcherViewModel>().getCurrentVersionString(context),
),
Row(
children: [
Material(
color: Theme.of(context).colorScheme.secondaryContainer,
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: InkWell(
onTap: () {
locator<PatcherViewModel>()
.searchSuggestedVersionOnWeb();
},
borderRadius:
const BorderRadius.all(Radius.circular(8)),
child: Container(
padding: const EdgeInsets.all(4),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
locator<PatcherViewModel>()
.getSuggestedVersionString(context),
style: TextStyle(
color: Theme.of(context)
.colorScheme
.onSecondaryContainer,
),
),
const SizedBox(width: 4),
Icon(
Icons.search,
size: 16,
color: Theme.of(context)
.colorScheme
.onSecondaryContainer,
),
],
),
),
),
),
],
),
],
),

View File

@ -7,9 +7,9 @@ import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
class PatchSelectorCard extends StatelessWidget {
const PatchSelectorCard({
Key? key,
super.key,
required this.onPressed,
}) : super(key: key);
});
final Function() onPressed;
@override

View File

@ -10,7 +10,7 @@ import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
// ignore: must_be_immutable
class PatchItem extends StatefulWidget {
PatchItem({
Key? key,
super.key,
required this.name,
required this.simpleName,
required this.description,
@ -23,7 +23,7 @@ class PatchItem extends StatefulWidget {
required this.onChanged,
required this.navigateToOptions,
required this.isChangeEnabled,
}) : super(key: key);
});
final String name;
final String simpleName;
final String description;
@ -48,13 +48,13 @@ class _PatchItemState extends State<PatchItem> {
Widget build(BuildContext context) {
widget.isSelected = widget.isSelected &&
(!widget.isUnsupported ||
widget._managerAPI.areExperimentalPatchesEnabled()) &&
!widget._managerAPI.isVersionCompatibilityCheckEnabled()) &&
!widget.hasUnsupportedPatchOption;
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Opacity(
opacity: widget.isUnsupported &&
widget._managerAPI.areExperimentalPatchesEnabled() == false
widget._managerAPI.isVersionCompatibilityCheckEnabled() == true
? 0.5
: 1,
child: CustomCard(
@ -66,7 +66,7 @@ class _PatchItemState extends State<PatchItem> {
),
onTap: () {
if (widget.isUnsupported &&
!widget._managerAPI.areExperimentalPatchesEnabled()) {
widget._managerAPI.isVersionCompatibilityCheckEnabled()) {
widget.isSelected = false;
widget.toast.showBottom('patchItem.unsupportedPatchVersion');
} else if (widget.isChangeEnabled) {
@ -80,7 +80,7 @@ class _PatchItemState extends State<PatchItem> {
setState(() {});
}
if (!widget.isUnsupported ||
widget._managerAPI.areExperimentalPatchesEnabled()) {
!widget._managerAPI.isVersionCompatibilityCheckEnabled()) {
widget.onChanged(widget.isSelected);
}
},
@ -99,7 +99,8 @@ class _PatchItemState extends State<PatchItem> {
),
onChanged: (newValue) {
if (widget.isUnsupported &&
!widget._managerAPI.areExperimentalPatchesEnabled()) {
widget._managerAPI
.isVersionCompatibilityCheckEnabled()) {
widget.isSelected = false;
widget.toast.showBottom(
'patchItem.unsupportedPatchVersion',
@ -115,7 +116,8 @@ class _PatchItemState extends State<PatchItem> {
setState(() {});
}
if (!widget.isUnsupported ||
widget._managerAPI.areExperimentalPatchesEnabled()) {
!widget._managerAPI
.isVersionCompatibilityCheckEnabled()) {
widget.onChanged(widget.isSelected);
}
},
@ -155,8 +157,8 @@ class _PatchItemState extends State<PatchItem> {
runSpacing: 4,
children: [
if (widget.isUnsupported &&
widget._managerAPI
.areExperimentalPatchesEnabled())
!widget._managerAPI
.isVersionCompatibilityCheckEnabled())
Padding(
padding: const EdgeInsets.only(top: 8),
child: TextButton.icon(

View File

@ -1,6 +1,6 @@
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_file_dialog/flutter_file_dialog.dart';
import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:revanced_manager/models/patch.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
@ -59,13 +59,27 @@ class IntAndStringPatchOption extends StatelessWidget {
@override
Widget build(BuildContext context) {
final ValueNotifier patchOptionValue = ValueNotifier(patchOption.value);
String getKey() {
if (patchOption.value != null && patchOption.values != null) {
final List values = patchOption.values!.entries
.where((e) => e.value == patchOption.value)
.toList();
if (values.isNotEmpty) {
return values.first.key;
}
}
return '';
}
return PatchOption(
widget: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextFieldForPatchOption(
value: patchOption.value,
optionType: patchOption.optionClassType,
values: patchOption.values,
optionType: patchOption.valueType,
selectedKey: getKey(),
onChanged: (value) {
patchOptionValue.value = value;
onChanged(value, patchOption);
@ -77,7 +91,7 @@ class IntAndStringPatchOption extends StatelessWidget {
if (patchOption.required && value == null) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children:[
children: [
const SizedBox(height: 8),
I18nText(
'patchOptionsView.requiredOption',
@ -119,17 +133,41 @@ class IntStringLongListPatchOption extends StatelessWidget {
@override
Widget build(BuildContext context) {
final String type = patchOption.optionClassType;
final List<dynamic> values = patchOption.value ?? [];
final List<dynamic> values = List.from(patchOption.value ?? []);
final ValueNotifier patchOptionValue = ValueNotifier(values);
final String type = patchOption.valueType;
String getKey(dynamic value) {
if (value != null && patchOption.values != null) {
final List values = patchOption.values!.entries
.where((e) => e.value.toString() == value)
.toList();
if (values.isNotEmpty) {
return values.first.key;
}
}
return '';
}
bool isCustomValue() {
if (values.length == 1 && patchOption.values != null) {
if (getKey(values[0]) != '') {
return false;
}
}
return true;
}
bool isTextFieldVisible = isCustomValue();
return PatchOption(
widget: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ValueListenableBuilder(
valueListenable: patchOptionValue,
builder: (context, value, child) {
return ListView.builder(
widget: ValueListenableBuilder(
valueListenable: patchOptionValue,
builder: (context, value, child) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ListView.builder(
shrinkWrap: true,
itemCount: value.length,
physics: const NeverScrollableScrollPhysics(),
@ -137,54 +175,89 @@ class IntStringLongListPatchOption extends StatelessWidget {
final e = values[index];
return TextFieldForPatchOption(
value: e.toString(),
values: patchOption.values,
optionType: type,
selectedKey: value.length > 1 ? '' : getKey(e),
showDropdown: index == 0,
onChanged: (newValue) {
values[index] = type == 'StringListPatchOption' ? newValue : type == 'IntListPatchOption' ? int.parse(newValue) : num.parse(newValue);
if (newValue is List) {
values.clear();
isTextFieldVisible = false;
values.add(newValue.toString());
} else {
isTextFieldVisible = true;
if (values.length == 1 &&
values[0].toString().startsWith('[') &&
type.contains('Array')) {
values.clear();
values.addAll(patchOption.value);
} else {
values[index] = type == 'StringArray'
? newValue
: type == 'IntArray'
? int.parse(
newValue.toString().isEmpty
? '0'
: newValue.toString(),
)
: num.parse(
newValue.toString().isEmpty
? '0'
: newValue.toString(),
);
}
}
patchOptionValue.value = List.from(values);
onChanged(values, patchOption);
},
removeValue: (value) {
patchOptionValue.value = List.from(patchOptionValue.value)..removeAt(index);
removeValue: () {
patchOptionValue.value = List.from(patchOptionValue.value)
..removeAt(index);
values.removeAt(index);
onChanged(values, patchOption);
},
);
},
);
},
),
const SizedBox(height: 4),
Align(
alignment: Alignment.centerLeft,
child: TextButton(
onPressed: () {
if (type == 'StringListPatchOption') {
patchOptionValue.value = List.from(patchOptionValue.value)..add('');
values.add('');
} else {
patchOptionValue.value = List.from(patchOptionValue.value)..add(0);
values.add(0);
}
onChanged(values, patchOption);
},
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.add, size: 20),
I18nText(
'add',
child: const Text(
'',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
),
],
),
),
),
],
if (isTextFieldVisible) ...[
const SizedBox(height: 4),
Align(
alignment: Alignment.centerLeft,
child: TextButton(
onPressed: () {
if (type == 'StringArray') {
patchOptionValue.value =
List.from(patchOptionValue.value)..add('');
values.add('');
} else {
patchOptionValue.value =
List.from(patchOptionValue.value)..add(0);
values.add(0);
}
onChanged(values, patchOption);
},
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.add, size: 20),
I18nText(
'add',
child: const Text(
'',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
),
],
),
),
),
],
],
);
},
),
patchOption: patchOption,
removeOption: (Option option) {
@ -196,6 +269,7 @@ class IntStringLongListPatchOption extends StatelessWidget {
class UnsupportedPatchOption extends StatelessWidget {
const UnsupportedPatchOption({super.key, required this.patchOption});
final Option patchOption;
@override
@ -239,7 +313,6 @@ class PatchOption extends StatelessWidget {
return Padding(
padding: const EdgeInsets.all(8.0),
child: CustomCard(
onTap: () {},
child: Row(
children: [
Expanded(
@ -296,14 +369,20 @@ class TextFieldForPatchOption extends StatefulWidget {
const TextFieldForPatchOption({
super.key,
required this.value,
required this.values,
this.removeValue,
required this.onChanged,
required this.optionType,
required this.selectedKey,
this.showDropdown = true,
});
final String? value;
final Map<String, dynamic>? values;
final String optionType;
final void Function(dynamic value)? removeValue;
final String selectedKey;
final bool showDropdown;
final void Function()? removeValue;
final void Function(dynamic value) onChanged;
@override
@ -313,75 +392,155 @@ class TextFieldForPatchOption extends StatefulWidget {
class _TextFieldForPatchOptionState extends State<TextFieldForPatchOption> {
final TextEditingController controller = TextEditingController();
String? selectedKey;
String? defaultValue;
@override
Widget build(BuildContext context) {
final bool isStringOption = widget.optionType.contains('String');
final bool isListOption = widget.optionType.contains('List');
controller.text = widget.value ?? '';
return TextFormField(
inputFormatters: [
if (widget.optionType.contains('Int'))
FilteringTextInputFormatter.allow(RegExp(r'[0-9]')),
if (widget.optionType.contains('Long'))
FilteringTextInputFormatter.allow(RegExp(r'^[0-9]*\.?[0-9]*')),
],
controller: controller,
keyboardType: isStringOption ? TextInputType.text : TextInputType.number,
decoration: InputDecoration(
suffixIcon: PopupMenuButton(
tooltip: FlutterI18n.translate(
context,
'patchOptionsView.tooltip',
final bool isArrayOption = widget.optionType.contains('Array');
selectedKey ??= widget.selectedKey;
controller.text = !isStringOption && isArrayOption && selectedKey == '' &&
(widget.value != null && widget.value.toString().startsWith('['))
? ''
: widget.value ?? '';
defaultValue ??= controller.text;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (widget.showDropdown && (widget.values?.isNotEmpty ?? false))
DropdownButton<String>(
style: const TextStyle(
fontSize: 16,
),
borderRadius: BorderRadius.circular(4),
dropdownColor: Theme.of(context).colorScheme.secondaryContainer,
isExpanded: true,
value: selectedKey,
items: widget.values!.entries
.map(
(e) => DropdownMenuItem(
value: e.key,
child: RichText(
text: TextSpan(
text: e.key,
style: const TextStyle(
fontSize: 16,
),
children: [
TextSpan(
text: ' ${e.value}',
style: TextStyle(
fontSize: 14,
color: Theme.of(context)
.colorScheme
.onSecondaryContainer
.withOpacity(0.6),
),
),
],
),
),
),
)
.toList()
..add(
DropdownMenuItem(
value: '',
child: I18nText(
'patchOptionsView.customValue',
child: const Text(
'',
style: TextStyle(
fontSize: 16,
),
),
),
),
),
onChanged: (value) {
if (value == '') {
controller.text = defaultValue!;
widget.onChanged(controller.text);
} else {
controller.text = widget.values![value].toString();
widget.onChanged(
isArrayOption ? widget.values![value] : controller.text,
);
}
setState(() {
selectedKey = value;
});
},
),
itemBuilder: (BuildContext context) {
return [
if (isListOption)
PopupMenuItem(
value: 'remove',
child: I18nText('remove'),
if (selectedKey == '')
TextFormField(
inputFormatters: [
if (widget.optionType.contains('Int'))
FilteringTextInputFormatter.allow(RegExp(r'[0-9]')),
if (widget.optionType.contains('Long'))
FilteringTextInputFormatter.allow(RegExp(r'^[0-9]*\.?[0-9]*')),
],
controller: controller,
keyboardType:
isStringOption ? TextInputType.text : TextInputType.number,
decoration: InputDecoration(
suffixIcon: PopupMenuButton(
tooltip: FlutterI18n.translate(
context,
'patchOptionsView.tooltip',
),
if (isStringOption && !isListOption) ...[
PopupMenuItem(
value: 'patchOptionsView.selectFilePath',
child: I18nText('patchOptionsView.selectFilePath'),
),
PopupMenuItem(
value: 'patchOptionsView.selectFolder',
child: I18nText('patchOptionsView.selectFolder'),
),
],
];
},
onSelected: (String selection) async {
switch (selection) {
case 'patchOptionsView.selectFilePath':
final result = await FilePicker.platform.pickFiles();
if (result != null && result.files.single.path != null) {
controller.text = result.files.single.path.toString();
widget.onChanged(controller.text);
}
break;
case 'patchOptionsView.selectFolder':
final result = await FilePicker.platform.getDirectoryPath();
if (result != null) {
controller.text = result;
widget.onChanged(controller.text);
}
break;
case 'remove':
widget.removeValue!(widget.value);
break;
}
},
),
hintStyle: TextStyle(
fontSize: 14,
color: Theme.of(context).colorScheme.onSecondaryContainer,
),
),
onChanged: (String value) {
widget.onChanged(value);
},
itemBuilder: (BuildContext context) {
return [
if (isArrayOption)
PopupMenuItem(
value: 'remove',
child: I18nText('remove'),
),
if (isStringOption) ...[
PopupMenuItem(
value: 'patchOptionsView.selectFilePath',
child: I18nText('patchOptionsView.selectFilePath'),
),
PopupMenuItem(
value: 'patchOptionsView.selectFolder',
child: I18nText('patchOptionsView.selectFolder'),
),
],
];
},
onSelected: (String selection) async {
switch (selection) {
case 'patchOptionsView.selectFilePath':
final String? result = await FlutterFileDialog.pickFile();
if (result != null) {
controller.text = result;
widget.onChanged(controller.text);
}
break;
case 'patchOptionsView.selectFolder':
final DirectoryLocation? result = await FlutterFileDialog.pickDirectory();
if (result != null) {
controller.text = result.toString();
widget.onChanged(controller.text);
}
break;
case 'remove':
widget.removeValue!();
break;
}
},
),
hintStyle: TextStyle(
fontSize: 14,
color: Theme.of(context).colorScheme.onSecondaryContainer,
),
),
onChanged: (String value) {
widget.onChanged(value);
},
),
],
);
}
}

View File

@ -4,7 +4,7 @@ import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:revanced_manager/utils/about_info.dart';
class AboutWidget extends StatefulWidget {
const AboutWidget({Key? key, this.padding}) : super(key: key);
const AboutWidget({super.key, this.padding});
final EdgeInsetsGeometry? padding;

View File

@ -2,10 +2,10 @@ import 'package:flutter/material.dart';
class CustomSwitch extends StatelessWidget {
const CustomSwitch({
Key? key,
super.key,
required this.onChanged,
required this.value,
}) : super(key: key);
});
final ValueChanged<bool> onChanged;
final bool value;

View File

@ -3,13 +3,13 @@ import 'package:revanced_manager/ui/widgets/settingsView/custom_switch.dart';
class CustomSwitchTile extends StatelessWidget {
const CustomSwitchTile({
Key? key,
super.key,
required this.title,
required this.subtitle,
required this.value,
required this.onTap,
this.padding,
}) : super(key: key);
});
final Widget title;
final Widget subtitle;
final bool value;

View File

@ -2,13 +2,13 @@ import 'package:flutter/material.dart';
class CustomTextField extends StatelessWidget {
const CustomTextField({
Key? key,
super.key,
required this.inputController,
required this.label,
required this.hint,
this.leadingIcon,
required this.onChanged,
}) : super(key: key);
});
final TextEditingController inputController;
final Widget label;
final String hint;

View File

@ -1,18 +1,15 @@
// ignore_for_file: prefer_const_constructors
import 'package:flutter/material.dart';
import 'package:flutter_i18n/widgets/I18nText.dart';
import 'package:revanced_manager/ui/views/settings/settingsFragment/settings_manage_api_url.dart';
import 'package:revanced_manager/ui/views/settings/settingsFragment/settings_manage_sources.dart';
import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_auto_update_patches.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_enable_patches_selection.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_experimental_patches.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_experimental_universal_patches.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_require_suggested_app_version.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_section.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_universal_patches.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_version_compatibility_check.dart';
final _settingsViewModel = SettingsViewModel();
class SAdvancedSection extends StatelessWidget {
const SAdvancedSection({super.key});
@ -21,85 +18,15 @@ class SAdvancedSection extends StatelessWidget {
Widget build(BuildContext context) {
return SettingsSection(
title: 'settingsView.advancedSectionTitle',
children: <Widget>[
SManageApiUrlUI(),
SManageSourcesUI(),
// SManageKeystorePasswordUI(),
children: const <Widget>[
SAutoUpdatePatches(),
SEnablePatchesSelection(),
SExperimentalUniversalPatches(),
SExperimentalPatches(),
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText(
'settingsView.regenerateKeystoreLabel',
child: const Text(
'',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
),
),
),
subtitle: I18nText('settingsView.regenerateKeystoreHint'),
onTap: () => _showDeleteKeystoreDialog(context),
),
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText(
'settingsView.deleteTempDirLabel',
child: const Text(
'',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
),
),
),
subtitle: I18nText('settingsView.deleteTempDirHint'),
onTap: () => _settingsViewModel.deleteTempDir(),
),
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText(
'settingsView.deleteLogsLabel',
child: const Text(
'',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
),
),
),
subtitle: I18nText('settingsView.deleteLogsHint'),
onTap: () => _settingsViewModel.deleteLogs(),
),
SRequireSuggestedAppVersion(),
SVersionCompatibilityCheck(),
SUniversalPatches(),
SManageSourcesUI(),
SManageApiUrlUI(),
],
);
}
Future<void> _showDeleteKeystoreDialog(context) {
return showDialog(
context: context,
builder: (context) => AlertDialog(
title: I18nText('settingsView.regenerateKeystoreDialogTitle'),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
content: I18nText('settingsView.regenerateKeystoreDialogText'),
actions: <Widget>[
CustomMaterialButton(
isFilled: false,
label: I18nText('noButton'),
onPressed: () => Navigator.of(context).pop(),
),
CustomMaterialButton(
label: I18nText('yesButton'),
onPressed: () => {
Navigator.of(context).pop(),
_settingsViewModel.deleteKeystore(),
},
),
],
),
);
}
}

View File

@ -0,0 +1,68 @@
import 'package:flutter/material.dart';
import 'package:flutter_i18n/widgets/I18nText.dart';
import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/settingsView/about_widget.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_section.dart';
final _settingsViewModel = SettingsViewModel();
class SDebugSection extends StatelessWidget {
const SDebugSection({super.key});
@override
Widget build(BuildContext context) {
return SettingsSection(
title: 'settingsView.debugSectionTitle',
children: <Widget>[
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText(
'settingsView.logsLabel',
child: const Text(
'',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
),
),
),
subtitle: I18nText('settingsView.logsHint'),
onTap: () => _settingsViewModel.exportLogcatLogs(),
),
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText(
'settingsView.deleteLogsLabel',
child: const Text(
'',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
),
),
),
subtitle: I18nText('settingsView.deleteLogsHint'),
onTap: () => _settingsViewModel.deleteLogs(),
),
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText(
'settingsView.deleteTempDirLabel',
child: const Text(
'',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
),
),
),
subtitle: I18nText('settingsView.deleteTempDirHint'),
onTap: () => _settingsViewModel.deleteTempDir(),
),
const AboutWidget(
padding: EdgeInsets.symmetric(horizontal: 20.0),
),
],
);
}
}

View File

@ -45,42 +45,6 @@ class SExportSection extends StatelessWidget {
subtitle: I18nText('settingsView.importPatchesHint'),
onTap: () => _settingsViewModel.importPatches(context),
),
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText(
'settingsView.exportKeystoreLabel',
child: const Text(
'',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
),
),
),
subtitle: I18nText('settingsView.exportKeystoreHint'),
onTap: () => _settingsViewModel.exportKeystore(),
),
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText(
'settingsView.importKeystoreLabel',
child: const Text(
'',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
),
),
),
subtitle: I18nText('settingsView.importKeystoreHint'),
onTap: () async {
await _settingsViewModel.importKeystore();
final sManageKeystorePassword = SManageKeystorePassword();
if (context.mounted) {
sManageKeystorePassword.showKeystoreDialog(context);
}
},
),
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText(
@ -121,6 +85,58 @@ class SExportSection extends StatelessWidget {
_settingsViewModel.resetAllOptions,
),
),
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText(
'settingsView.exportKeystoreLabel',
child: const Text(
'',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
),
),
),
subtitle: I18nText('settingsView.exportKeystoreHint'),
onTap: () => _settingsViewModel.exportKeystore(),
),
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText(
'settingsView.importKeystoreLabel',
child: const Text(
'',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
),
),
),
subtitle: I18nText('settingsView.importKeystoreHint'),
onTap: () async {
await _settingsViewModel.importKeystore();
final sManageKeystorePassword = SManageKeystorePassword();
if (context.mounted) {
sManageKeystorePassword.showKeystoreDialog(context);
}
},
),
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText(
'settingsView.regenerateKeystoreLabel',
child: const Text(
'',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
),
),
),
subtitle: I18nText('settingsView.regenerateKeystoreHint'),
onTap: () => _showDeleteKeystoreDialog(context),
),
// SManageKeystorePasswordUI(),
],
);
}
@ -154,4 +170,29 @@ class SExportSection extends StatelessWidget {
),
);
}
Future<void> _showDeleteKeystoreDialog(context) {
return showDialog(
context: context,
builder: (context) => AlertDialog(
title: I18nText('settingsView.regenerateKeystoreDialogTitle'),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
content: I18nText('settingsView.regenerateKeystoreDialogText'),
actions: <Widget>[
CustomMaterialButton(
isFilled: false,
label: I18nText('noButton'),
onPressed: () => Navigator.of(context).pop(),
),
CustomMaterialButton(
label: I18nText('yesButton'),
onPressed: () => {
Navigator.of(context).pop(),
_settingsViewModel.deleteKeystore(),
},
),
],
),
);
}
}

View File

@ -1,38 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_i18n/widgets/I18nText.dart';
import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/settingsView/about_widget.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_section.dart';
final _settingsViewModel = SettingsViewModel();
class SInfoSection extends StatelessWidget {
const SInfoSection({super.key});
@override
Widget build(BuildContext context) {
return SettingsSection(
title: 'settingsView.infoSectionTitle',
children: <Widget>[
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText(
'settingsView.logsLabel',
child: const Text(
'',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
),
),
),
subtitle: I18nText('settingsView.logsHint'),
onTap: () => _settingsViewModel.exportLogcatLogs(),
),
const AboutWidget(
padding: EdgeInsets.symmetric(horizontal: 20.0),
),
],
);
}
}

View File

@ -0,0 +1,37 @@
import 'package:flutter/material.dart';
import 'package:flutter_i18n/widgets/I18nText.dart';
import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart';
class SRequireSuggestedAppVersion extends StatefulWidget {
const SRequireSuggestedAppVersion({super.key});
@override
State<SRequireSuggestedAppVersion> createState() => _SRequireSuggestedAppVersionState();
}
final _settingsViewModel = SettingsViewModel();
class _SRequireSuggestedAppVersionState extends State<SRequireSuggestedAppVersion> {
@override
Widget build(BuildContext context) {
return SwitchListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText(
'settingsView.requireSuggestedAppVersionLabel',
child: const Text(
'',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
),
),
),
subtitle: I18nText('settingsView.requireSuggestedAppVersionHint'),
value: _settingsViewModel.isRequireSuggestedAppVersionEnabled(),
onChanged: (value) async {
await _settingsViewModel.showRequireSuggestedAppVersionDialog(context, value,);
setState(() {});
},
);
}
}

View File

@ -3,10 +3,10 @@ import 'package:flutter_i18n/flutter_i18n.dart';
class SettingsSection extends StatelessWidget {
const SettingsSection({
Key? key,
super.key,
required this.title,
required this.children,
}) : super(key: key);
});
final String title;
final List<Widget> children;

View File

@ -3,12 +3,12 @@ import 'package:flutter_i18n/flutter_i18n.dart';
class SettingsTileDialog extends StatelessWidget {
const SettingsTileDialog({
Key? key,
super.key,
required this.title,
required this.subtitle,
required this.onTap,
this.padding,
}) : super(key: key);
});
final String title;
final String subtitle;
final Function()? onTap;

View File

@ -4,26 +4,26 @@ import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
import 'package:revanced_manager/ui/views/patches_selector/patches_selector_viewmodel.dart';
import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart';
class SExperimentalUniversalPatches extends StatefulWidget {
const SExperimentalUniversalPatches({super.key});
class SUniversalPatches extends StatefulWidget {
const SUniversalPatches({super.key});
@override
State<SExperimentalUniversalPatches> createState() =>
_SExperimentalUniversalPatchesState();
State<SUniversalPatches> createState() =>
_SUniversalPatchesState();
}
final _settingsViewModel = SettingsViewModel();
final _patchesSelectorViewModel = PatchesSelectorViewModel();
final _patcherViewModel = PatcherViewModel();
class _SExperimentalUniversalPatchesState
extends State<SExperimentalUniversalPatches> {
class _SUniversalPatchesState
extends State<SUniversalPatches> {
@override
Widget build(BuildContext context) {
return SwitchListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText(
'settingsView.experimentalUniversalPatchesLabel',
'settingsView.universalPatchesLabel',
child: const Text(
'',
style: TextStyle(
@ -32,7 +32,7 @@ class _SExperimentalUniversalPatchesState
),
),
),
subtitle: I18nText('settingsView.experimentalUniversalPatchesHint'),
subtitle: I18nText('settingsView.universalPatchesHint'),
value: _settingsViewModel.areUniversalPatchesEnabled(),
onChanged: (value) {
setState(() {

View File

@ -5,24 +5,24 @@ import 'package:revanced_manager/ui/views/patches_selector/patches_selector_view
import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart';
import 'package:revanced_manager/utils/check_for_supported_patch.dart';
class SExperimentalPatches extends StatefulWidget {
const SExperimentalPatches({super.key});
class SVersionCompatibilityCheck extends StatefulWidget {
const SVersionCompatibilityCheck({super.key});
@override
State<SExperimentalPatches> createState() => _SExperimentalPatchesState();
State<SVersionCompatibilityCheck> createState() => _SVersionCompatibilityCheckState();
}
final _settingsViewModel = SettingsViewModel();
final _patchesSelectorViewModel = PatchesSelectorViewModel();
final _patcherViewModel = PatcherViewModel();
class _SExperimentalPatchesState extends State<SExperimentalPatches> {
class _SVersionCompatibilityCheckState extends State<SVersionCompatibilityCheck> {
@override
Widget build(BuildContext context) {
return SwitchListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText(
'settingsView.experimentalPatchesLabel',
'settingsView.versionCompatibilityCheckLabel',
child: const Text(
'',
style: TextStyle(
@ -31,11 +31,11 @@ class _SExperimentalPatchesState extends State<SExperimentalPatches> {
),
),
),
subtitle: I18nText('settingsView.experimentalPatchesHint'),
value: _settingsViewModel.areExperimentalPatchesEnabled(),
subtitle: I18nText('settingsView.versionCompatibilityCheckHint'),
value: _settingsViewModel.isVersionCompatibilityCheckEnabled(),
onChanged: (value) {
setState(() {
_settingsViewModel.useExperimentalPatches(value);
_settingsViewModel.useVersionCompatibilityCheck(value);
});
if (!value) {
_patcherViewModel.selectedPatches

View File

@ -3,12 +3,12 @@ import 'package:url_launcher/url_launcher.dart';
class SocialMediaItem extends StatelessWidget {
const SocialMediaItem({
Key? key,
super.key,
this.icon,
required this.title,
this.subtitle,
this.url,
}) : super(key: key);
});
final Widget? icon;
final Widget title;
final Widget? subtitle;

View File

@ -4,12 +4,13 @@ import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:revanced_manager/ui/widgets/settingsView/social_media_item.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_icon.dart';
class SocialMediaWidget extends StatelessWidget {
const SocialMediaWidget({
Key? key,
super.key,
this.padding,
}) : super(key: key);
});
final EdgeInsetsGeometry? padding;
@override
@ -42,11 +43,17 @@ class SocialMediaWidget extends StatelessWidget {
child: const CustomCard(
child: Column(
children: <Widget>[
SocialMediaItem(
icon: Icon(CustomIcon.revancedIcon),
title: Text('Website'),
subtitle: Text('revanced.app'),
url: 'https://revanced.app',
),
SocialMediaItem(
icon: FaIcon(FontAwesomeIcons.github),
title: Text('GitHub'),
subtitle: Text('github.com/revanced'),
url: 'https://github.com/revanced',
subtitle: Text('github.com/ReVanced'),
url: 'https://github.com/ReVanced',
),
SocialMediaItem(
icon: FaIcon(FontAwesomeIcons.discord),
@ -67,10 +74,10 @@ class SocialMediaWidget extends StatelessWidget {
url: 'https://reddit.com/r/revancedapp',
),
SocialMediaItem(
icon: FaIcon(FontAwesomeIcons.twitter),
title: Text('Twitter'),
icon: FaIcon(FontAwesomeIcons.xTwitter),
title: Text('X'),
subtitle: Text('@revancedapp'),
url: 'https://twitter.com/revancedapp',
url: 'https://x.com/revancedapp',
),
SocialMediaItem(
icon: FaIcon(FontAwesomeIcons.youtube),

View File

@ -8,12 +8,12 @@ import 'package:timeago/timeago.dart';
class ApplicationItem extends StatefulWidget {
const ApplicationItem({
Key? key,
super.key,
required this.icon,
required this.name,
required this.patchDate,
required this.onPressed,
}) : super(key: key);
});
final Uint8List icon;
final String name;
final DateTime patchDate;

View File

@ -2,13 +2,13 @@ import 'package:flutter/material.dart';
class CustomCard extends StatelessWidget {
const CustomCard({
Key? key,
super.key,
this.isFilled = true,
required this.child,
this.onTap,
this.padding,
this.backgroundColor,
}) : super(key: key);
});
final bool isFilled;
final Widget child;
final Function()? onTap;

View File

@ -2,11 +2,11 @@ import 'package:flutter/material.dart';
class CustomChip extends StatelessWidget {
const CustomChip({
Key? key,
super.key,
required this.label,
this.isSelected = false,
this.onSelected,
}) : super(key: key);
});
final Widget label;
final bool isSelected;
final Function(bool)? onSelected;

View File

@ -0,0 +1,9 @@
import 'package:flutter/widgets.dart';
class CustomIcon {
CustomIcon._();
static const _kFontFam = 'CustomIcon';
static const IconData revancedIcon = IconData(0xe800, fontFamily: _kFontFam);
}

View File

@ -2,12 +2,12 @@ import 'package:flutter/material.dart';
class CustomMaterialButton extends StatelessWidget {
const CustomMaterialButton({
Key? key,
super.key,
required this.label,
this.isFilled = true,
this.isExpanded = false,
required this.onPressed,
}) : super(key: key);
});
final Widget label;
final bool isFilled;
final bool isExpanded;
@ -49,13 +49,13 @@ class CustomMaterialButton extends StatelessWidget {
// ignore: must_be_immutable
class TimerButton extends StatefulWidget {
TimerButton({
Key? key,
super.key,
required this.seconds,
required this.isRunning,
required this.onTimerEnd,
this.label = const Text(''),
this.isFilled = true,
}) : super(key: key);
});
Widget label;
bool isFilled;
int seconds;

View File

@ -1,39 +0,0 @@
import 'package:flutter/material.dart';
class CustomPopupMenu extends StatelessWidget {
const CustomPopupMenu({
Key? key,
required this.onSelected,
required this.children,
}) : super(key: key);
final Function(dynamic) onSelected;
final Map<int, Widget> children;
@override
Widget build(BuildContext context) {
return Theme(
data: Theme.of(context).copyWith(useMaterial3: false),
child: PopupMenuButton<int>(
icon: Icon(
Icons.more_vert,
color: Theme.of(context).colorScheme.secondary,
),
onSelected: onSelected,
itemBuilder: (context) => children.entries
.map(
(entry) => PopupMenuItem<int>(
padding: const EdgeInsets.all(16.0).copyWith(right: 20),
value: entry.key,
child: entry.value,
),
)
.toList(),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(24),
),
color: Theme.of(context).colorScheme.secondaryContainer,
position: PopupMenuPosition.under,
),
);
}
}

View File

@ -2,13 +2,13 @@ import 'package:flutter/material.dart';
class CustomSliverAppBar extends StatelessWidget {
const CustomSliverAppBar({
Key? key,
super.key,
required this.title,
this.actions,
this.bottom,
this.isMainView = false,
this.onBackButtonPressed,
}) : super(key: key);
});
final Widget title;
final List<Widget>? actions;
final PreferredSizeWidget? bottom;

View File

@ -3,10 +3,10 @@ import 'package:flutter/material.dart';
class OpenContainerWrapper extends StatelessWidget {
const OpenContainerWrapper({
Key? key,
super.key,
required this.openBuilder,
required this.closedBuilder,
}) : super(key: key);
});
final OpenContainerBuilder openBuilder;
final CloseContainerBuilder closedBuilder;

View File

@ -2,12 +2,12 @@ import 'package:flutter/material.dart';
class SearchBar extends StatefulWidget {
const SearchBar({
Key? key,
super.key,
required this.hintText,
this.showSelectIcon = false,
this.onSelectAll,
required this.onQueryChanged,
}) : super(key: key);
});
final String? hintText;
final bool showSelectIcon;
final Function(bool)? onSelectAll;

View File

@ -17,12 +17,12 @@ bool isPatchSupported(Patch patch) {
bool hasUnsupportedRequiredOption(List<Option> options, Patch patch) {
final List<String> requiredOptionsType = [];
final List<String> supportedOptionsType = [
'StringPatchOption',
'BooleanPatchOption',
'IntPatchOption',
'StringListPatchOption',
'IntListPatchOption',
'LongListPatchOption',
'String',
'Boolean',
'Int',
'StringArray',
'IntArray',
'LongArray',
];
for (final Option option in options) {
if (option.required &&
@ -33,7 +33,7 @@ bool hasUnsupportedRequiredOption(List<Option> options, Patch patch) {
patch.name,
option.key,
) == null) {
requiredOptionsType.add(option.optionClassType);
requiredOptionsType.add(option.valueType);
}
}
for (final String optionType in requiredOptionsType) {

7179
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +0,0 @@
{
"devDependencies": {
"@saithodev/semantic-release-backmerge": "^3.1.0",
"@semantic-release/changelog": "^6.0.3",
"@semantic-release/git": "^10.0.1",
"semantic-release": "^21.0.1",
"semantic-release-export-data": "^1.0.1",
"semantic-release-flutter-plugin": "^1.1.2"
}
}

View File

@ -4,7 +4,7 @@ homepage: https://github.com/revanced/revanced-manager
publish_to: 'none'
version: 1.12.1+101200100
version: 1.16.0+101600000
environment:
sdk: '>=3.0.0 <4.0.0'
@ -12,84 +12,92 @@ environment:
dependencies:
animations: ^2.0.7
collection: ^1.17.0
cross_connectivity: ^3.0.5
cr_file_saver:
git:
url: https://github.com/dhruvanbhalara/cr_file_saver
ref: "fix/incorrect_file_name"
device_apps:
git:
url: https://github.com/ponces/flutter_plugin_device_apps
git: # switch back to ponces fork once https://github.com/ponces/flutter_plugin_device_apps/pull/1 is merged
url: https://github.com/BenjaminHalko/flutter_plugin_device_apps
ref: revanced-manager
device_info_plus: ^8.1.0
device_info_plus: ^9.1.0
dynamic_color: ^1.6.3
dio: ^5.0.0
dynamic_themes: ^1.1.0
expandable: ^5.0.1
file_picker:
git:
url: https://github.com/alexmercerind/flutter_file_picker
ref: master
flex_color_scheme: ^7.0.1
flutter:
sdk: flutter
flutter_background: ^1.2.0
flutter_background:
git: # remove once https://github.com/JulianAssmann/flutter_background/pull/79 is merged
url: https://github.com/BenjaminHalko/flutter_background
ref: specify-namespace
flutter_cache_manager: ^3.3.0
flutter_i18n: ^0.33.0
flutter_local_notifications: ^13.0.0
flutter_i18n: ^0.34.0
flutter_local_notifications: ^16.1.0
flutter_localizations:
sdk: flutter
flutter_svg: ^2.0.4
fluttertoast: ^8.2.1
fluttertoast:
git: # remove once the next fluttertoast version is release (> 8.2.2)
url: https://github.com/ponnamkarthik/FlutterToast
ref: f4e7b4e1afc8c760eb5bac80f6a2e299906d83ca
font_awesome_flutter: ^10.4.0
get_it: 7.2.0
google_fonts: ^4.0.3
http: ^0.13.5
get_it: ^7.6.4
google_fonts: ^6.1.0
http: ^1.1.0
injectable: ^2.1.1
intl: ^0.18.0
json_annotation: ^4.8.0
logcat:
git:
url: https://github.com/SuaMusica/logcat
ref: feature/nullSafe
package_info_plus: ^3.0.3
url: https://github.com/BenjaminHalko/logcat
ref: master
package_info_plus: ^4.2.0
path_provider: ^2.0.14
permission_handler: ^10.2.0
permission_handler: ^11.0.1
pull_to_refresh: ^2.0.0
root:
git:
url: https://github.com/EvadeMaster/root
ref: 82803aa40f63cddff81c3e4d27ce8ce3e7c83f60
share_extend: ^2.0.0
url: https://github.com/validcube/root
ref: 68e5678a535a2a3344828a14a39017fa74b9098c
shared_preferences: ^2.1.0
skeletons: ^0.0.3
stacked: ^3.2.0
stacked_generator: ^1.1.0
stacked_services: ^1.0.0
stacked_themes: ^0.3.10
timeago: ^3.3.0
timezone: ^0.9.0
url_launcher: ^6.1.10
wakelock: ^0.6.2
flutter_dotenv: ^5.0.2
flutter_markdown: ^0.6.14
dio_cache_interceptor: ^3.4.0
install_plugin: ^2.1.0
screenshot_callback: ^3.0.1
install_plugin:
git: # remove once https://github.com/hui-z/flutter_install_plugin/pull/67 is merged
url: https://github.com/BenjaminHalko/flutter_install_plugin
ref: master
screenshot_callback:
git: # remove once https://github.com/flutter-moum/flutter_screenshot_callback/pull/81 is merged
url: https://github.com/BenjaminHalko/flutter_screenshot_callback
ref: master
synchronized: ^3.1.0
connectivity_plus: ^5.0.1
flutter_file_dialog: ^3.0.2
wakelock_plus: ^1.1.3
share_plus: ^7.2.1
dev_dependencies:
json_serializable: ^6.6.1
build_runner: any
flutter_launcher_icons: ^0.13.0
flutter_lints: ^2.0.1
flutter_lints: ^3.0.1
flutter_test:
sdk: flutter
injectable_generator: ^2.1.5
injectable_generator: ^2.1.5
flutter:
uses-material-design: true
fonts:
- family: CustomIcon
fonts:
- asset: fonts/custom-icons.ttf
assets:
- assets/i18n/

View File

@ -1,2 +0,0 @@
include ':build.gradle'
project(':build.gradle').projectDir = new File(rootDir, 'android/build.gradle')