mirror of
https://github.com/ReVanced/revanced-manager.git
synced 2026-01-22 10:43:56 +00:00
Compare commits
126 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a12e8f37a | ||
|
|
59adb91f5f | ||
|
|
53677e2f39 | ||
|
|
1c74f43b22 | ||
|
|
b4801970e8 | ||
|
|
7a3a6b512f | ||
|
|
72ea33b6de | ||
|
|
d97192e0ee | ||
|
|
196d9fe4d2 | ||
|
|
e960fcc303 | ||
|
|
f334da95ff | ||
|
|
5d5f311e36 | ||
|
|
d577e97758 | ||
|
|
2dc92e26d3 | ||
|
|
f4994a36a3 | ||
|
|
7a785a8163 | ||
|
|
6ad0d860c7 | ||
|
|
38a2fa55df | ||
|
|
a21b170b52 | ||
|
|
44265b2362 | ||
|
|
069193342b | ||
|
|
54e9a56cda | ||
|
|
39bc9227dc | ||
|
|
ac636670c3 | ||
|
|
2abadc73e4 | ||
|
|
377368f6bf | ||
|
|
4085c10bfc | ||
|
|
657ba11e7e | ||
|
|
a9ae45fe63 | ||
|
|
61bb39b46f | ||
|
|
2ad106f7d7 | ||
|
|
8fd4fe0e55 | ||
|
|
b1c9aedac3 | ||
|
|
a80415be02 | ||
|
|
d9acd0d74b | ||
|
|
7ae09159ba | ||
|
|
a709abd80c | ||
|
|
cd07f39b69 | ||
|
|
f7c11d07a8 | ||
|
|
b07439d402 | ||
|
|
d8eadc2a2d | ||
|
|
3a88d4d3e6 | ||
|
|
012110f008 | ||
|
|
4de274bf62 | ||
|
|
76b89baee3 | ||
|
|
697ae92031 | ||
|
|
c87f92b346 | ||
|
|
6961bb7fd0 | ||
|
|
6e26130744 | ||
|
|
123a375a27 | ||
|
|
2b4b3ca0a5 | ||
|
|
c4a795418f | ||
|
|
91837ebade | ||
|
|
0492e910ea | ||
|
|
36c86e22b1 | ||
|
|
6bdc0c7bb2 | ||
|
|
1e8d8f749a | ||
|
|
2e8e3b0d1e | ||
|
|
15b8613d3c | ||
|
|
8ce266bc94 | ||
|
|
8661d72e45 | ||
|
|
62505f2543 | ||
|
|
37986c58ec | ||
|
|
2968d96fe9 | ||
|
|
e7c8d0e78c | ||
|
|
83cbb34a5b | ||
|
|
7559c7b67e | ||
|
|
02822f4b38 | ||
|
|
96736afb94 | ||
|
|
72ae132fcd | ||
|
|
2250e1bcab | ||
|
|
d9d5b746c3 | ||
|
|
f1ea306291 | ||
|
|
378d62395a | ||
|
|
99c92069b9 | ||
|
|
2a89ef797f | ||
|
|
5838550188 | ||
|
|
e0e01ae3ee | ||
|
|
0983ba8a0f | ||
|
|
0bfa776ce7 | ||
|
|
d2b09936d1 | ||
|
|
68e9f0f7c1 | ||
|
|
c3d345de80 | ||
|
|
385c0e246a | ||
|
|
5ead49a5b7 | ||
|
|
c0760b1347 | ||
|
|
e01b323aee | ||
|
|
6f4866ef63 | ||
|
|
1b6d72661c | ||
|
|
c59d4aea81 | ||
|
|
6260a80738 | ||
|
|
e75d3c8273 | ||
|
|
b7acb475e9 | ||
|
|
42b6bbff7c | ||
|
|
4b8542b35b | ||
|
|
9ad1d6cbfb | ||
|
|
4cdd9acd73 | ||
|
|
f4b0a695d6 | ||
|
|
b525ea1ba4 | ||
|
|
c1fc2c4766 | ||
|
|
5c733932c7 | ||
|
|
d1218616ec | ||
|
|
2bf6a03d56 | ||
|
|
b6ee63c1ea | ||
|
|
6d08efdcd7 | ||
|
|
a0a43a5651 | ||
|
|
3af2f5b032 | ||
|
|
8f54b226b4 | ||
|
|
9f64011b26 | ||
|
|
c5fc54e721 | ||
|
|
fc8a4fc5b6 | ||
|
|
6f9ab232ae | ||
|
|
8cb96f1e45 | ||
|
|
5733acb77a | ||
|
|
e49bcb2a69 | ||
|
|
42e41c399f | ||
|
|
166a3180d3 | ||
|
|
3bf4982f23 | ||
|
|
f4e1cccfac | ||
|
|
7911a8f49e | ||
|
|
64a96fc3ce | ||
|
|
8e2cfbddc5 | ||
|
|
45fae3f0fd | ||
|
|
e45a7824c1 | ||
|
|
5d72c48a76 | ||
|
|
d6169c6fa2 |
97
.github/ISSUE_TEMPLATE/bug-issue.yml
vendored
97
.github/ISSUE_TEMPLATE/bug-issue.yml
vendored
@@ -1,5 +1,5 @@
|
|||||||
name: 🐞 Bug report
|
name: 🐞 Bug report
|
||||||
description: Report a very clearly broken issue.
|
description: Create a new bug report.
|
||||||
title: 'bug: <title>'
|
title: 'bug: <title>'
|
||||||
labels: [bug]
|
labels: [bug]
|
||||||
body:
|
body:
|
||||||
@@ -8,53 +8,20 @@ body:
|
|||||||
value: |
|
value: |
|
||||||
# ReVanced Manager bug report
|
# ReVanced Manager bug report
|
||||||
|
|
||||||
Important to note that your issue may have already been reported before. Please check for existing issues [here](https://github.com/revanced/revanced-manager/labels/bug).
|
Please check for existing issues [here](https://github.com/revanced/revanced-manager/labels/bug) before creating a new one.
|
||||||
|
|
||||||
- type: dropdown
|
|
||||||
attributes:
|
|
||||||
label: Type
|
|
||||||
options:
|
|
||||||
- Error while running the manager
|
|
||||||
- Error at runtime
|
|
||||||
- Cosmetic
|
|
||||||
- Other
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: Bug description
|
label: Bug description
|
||||||
description: How did you find the bug? Any additional details that might help?
|
description: |
|
||||||
|
- Describe your bug in detail
|
||||||
|
- Add steps to reproduce the bug if possible (Step 1. Download some files. Step 2. ...)
|
||||||
|
- Add images and videos if possible
|
||||||
|
- List selected patches if applicable
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: Steps to reproduce
|
label: Version of ReVanced Manager and version & name of application you tried to patch
|
||||||
description: Add the steps to reproduce this bug, including your environment.
|
|
||||||
placeholder: Step 1. Download some files. Step 2. ...
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Android version
|
|
||||||
description: Android version used.
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Manager version
|
|
||||||
description: Manager version used.
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Target package name
|
|
||||||
description: App you tried to patch.
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Target package version.
|
|
||||||
description: Version of the app you tried to patch.
|
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
@@ -64,57 +31,31 @@ body:
|
|||||||
- Non-root
|
- Non-root
|
||||||
- Root
|
- Root
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: false
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: Patches selected.
|
label: Device logs
|
||||||
description: Patches you selected for the app.
|
description: Export logs in ReVanced Manager settings.
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Device logs (exported using Manager settings).
|
|
||||||
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so there is no need for backticks.
|
|
||||||
render: shell
|
render: shell
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: Installer logs (exported using Installer menu option) [unneeded if the issue is not during patching].
|
label: Patcher logs
|
||||||
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so there is no need for backticks.
|
description: Export logs in "Patcher" screen.
|
||||||
render: shell
|
render: shell
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Screenshots or video
|
|
||||||
description: Add screenshots or videos that show the bug here.
|
|
||||||
placeholder: Drag and drop the screenshots/videos into this box.
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Solution
|
|
||||||
description: If applicable, add a possible solution.
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Additional context
|
|
||||||
description: Add additional context here.
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
id: acknowledgments
|
|
||||||
attributes:
|
attributes:
|
||||||
label: Acknowledgments
|
label: Acknowledgements
|
||||||
description: Your issue will be closed if you haven't done these steps.
|
description: Your issue will be closed if you don't follow the checklist below!
|
||||||
options:
|
options:
|
||||||
- label: I have searched the existing issues; this is new and no duplicate or related to another open issue.
|
- label: This request is not a duplicate of an existing issue.
|
||||||
required: true
|
required: true
|
||||||
- label: I have written a short but informative title.
|
- label: I have chosen an appropriate title.
|
||||||
required: true
|
required: true
|
||||||
- label: I properly filled out all of the requested information in this issue.
|
- label: All requested information has been provided properly.
|
||||||
required: true
|
required: true
|
||||||
- label: The issue is solely related to ReVanced Manager and not caused by patches.
|
- label: The issue is solely related to the ReVanced Manager
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
40
.github/ISSUE_TEMPLATE/feature-issue.yml
vendored
40
.github/ISSUE_TEMPLATE/feature-issue.yml
vendored
@@ -1,52 +1,42 @@
|
|||||||
name: ⭐ Feature request
|
name: ⭐ Feature request
|
||||||
description: Create a detailed feature request.
|
description: Create a new feature request.
|
||||||
title: 'feat: <title>'
|
title: 'feat: <title>'
|
||||||
labels: [feature-request]
|
labels: [feature-request]
|
||||||
body:
|
body:
|
||||||
- type: dropdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
label: Type
|
value: |
|
||||||
options:
|
# ReVanced Manager feature request
|
||||||
- Functionality
|
|
||||||
- Cosmetic
|
Please check for existing feature requests [here](https://github.com/revanced/revanced-manager/labels/bug) before creating a new one.
|
||||||
- Other
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: Issue
|
label: Feature description
|
||||||
description: What is the current problem. Why does it require a feature request?
|
description: Describe your feature in detail.
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Feature
|
|
||||||
description: Describe your feature in detail. How does it solve the issue?
|
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: Motivation
|
label: Motivation
|
||||||
description: Why should your feature should be considered?
|
description: Explain why the lack of it is a problem.
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: Additional context
|
label: Additional context
|
||||||
description: Add additional context here.
|
description: In case there is something else you want to add.
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
id: acknowledgements
|
|
||||||
attributes:
|
attributes:
|
||||||
label: Acknowledgements
|
label: Acknowledgements
|
||||||
description: Your issue will be closed if you haven't done these steps.
|
description: Your issue will be closed if you don't follow the checklist below!
|
||||||
options:
|
options:
|
||||||
- label: I have searched the existing issues and this is a new and no duplicate or related to another open issue.
|
- label: This request is not a duplicate of an existing issue.
|
||||||
required: true
|
required: true
|
||||||
- label: I have written a short but informative title.
|
- label: I have chosen an appropriate title.
|
||||||
required: true
|
required: true
|
||||||
- label: I filled out all of the requested information in this issue properly.
|
- label: All requested information has been provided properly.
|
||||||
required: true
|
required: true
|
||||||
- label: The issue is related solely to the ReVanced Manager
|
- label: The issue is solely related to the ReVanced Manager
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
38
.github/workflows/analyze.yml
vendored
38
.github/workflows/analyze.yml
vendored
@@ -1,38 +0,0 @@
|
|||||||
name: Analyze Code
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ "dev" ]
|
|
||||||
paths:
|
|
||||||
- "**.dart"
|
|
||||||
- ".github/workflows/analyze.yml"
|
|
||||||
pull_request:
|
|
||||||
branches: [ "main", "dev" ]
|
|
||||||
types:
|
|
||||||
- opened
|
|
||||||
- reopened
|
|
||||||
- synchronize
|
|
||||||
- ready_for_review
|
|
||||||
paths:
|
|
||||||
- "**.dart"
|
|
||||||
- ".github/workflows/analyze.yml"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
name: "Static analysis & format check"
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- name: Setup Flutter
|
|
||||||
uses: subosito/flutter-action@v2
|
|
||||||
with:
|
|
||||||
channel: 'stable'
|
|
||||||
cache: true
|
|
||||||
- name: Install Flutter dependencies
|
|
||||||
run: flutter pub get
|
|
||||||
- name: Generate files with Builder
|
|
||||||
run: flutter packages pub run build_runner build --delete-conflicting-outputs
|
|
||||||
- name: Analyze code
|
|
||||||
uses: ValentinVignal/action-dart-analyze@v0.15
|
|
||||||
with:
|
|
||||||
fail-on: warning
|
|
||||||
2
.github/workflows/pr-build.yml
vendored
2
.github/workflows/pr-build.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
# Make sure the release step uses its own credentials:
|
# Make sure the release step uses its own credentials:
|
||||||
# https://github.com/cycjimmy/semantic-release-action#private-packages
|
# https://github.com/cycjimmy/semantic-release-action#private-packages
|
||||||
|
|||||||
2
.github/workflows/release-build.yml
vendored
2
.github/workflows/release-build.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
|||||||
release:
|
release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- name: Set env
|
- name: Set env
|
||||||
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
||||||
- name: Set up JDK 11
|
- name: Set up JDK 11
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -58,6 +58,7 @@ unlinked.ds
|
|||||||
unlinked_spec.ds
|
unlinked_spec.ds
|
||||||
|
|
||||||
# Android related
|
# Android related
|
||||||
|
.gradle/
|
||||||
**/android/**/gradle-wrapper.jar
|
**/android/**/gradle-wrapper.jar
|
||||||
**/android/.gradle
|
**/android/.gradle
|
||||||
**/android/captures/
|
**/android/captures/
|
||||||
|
|||||||
22
README.md
22
README.md
@@ -3,29 +3,33 @@
|
|||||||
The official ReVanced Manager based on Flutter.
|
The official ReVanced Manager based on Flutter.
|
||||||
|
|
||||||
## 🔽 Download
|
## 🔽 Download
|
||||||
To download latest Manager, go [here](https://github.com/revanced/revanced-manager/releases/latest) and install the provided APK file.
|
|
||||||
|
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)
|
||||||
|
|
||||||
## 📝 Prerequisites
|
## 📝 Prerequisites
|
||||||
|
|
||||||
1. Android 8 or higher
|
1. Android 8 or higher
|
||||||
2. Does not work on some armv7 devices
|
2. Incompatible with certain ARMv7 devices
|
||||||
|
|
||||||
|
## 📃 Documentation
|
||||||
|
The documentation can be found [here](https://github.com/revanced/revanced-manager/tree/main/docs).
|
||||||
|
|
||||||
## 🔴 Issues
|
## 🔴 Issues
|
||||||
|
|
||||||
For suggestions and bug reports, open an issue [here](https://github.com/revanced/revanced-manager/issues/new/choose).
|
For suggestions and bug reports, open an issue [here](https://github.com/revanced/revanced-manager/issues/new/choose).
|
||||||
|
|
||||||
## 💭 Discussion
|
|
||||||
If you wish to discuss the Manager, a thread has been made under the [#development](https://discord.com/channels/952946952348270622/1002922226443632761) channel in the Discord server, please note that this thread may be temporary and may be removed in the future.
|
|
||||||
|
|
||||||
|
|
||||||
## 🌐 Translation
|
## 🌐 Translation
|
||||||
|
|
||||||
[](https://crowdin.com/project/revanced)
|
[](https://crowdin.com/project/revanced)
|
||||||
|
|
||||||
If you wish to translate ReVanced Manager, we're accepting translations on [Crowdin](https://translate.revanced.app)
|
We're accepting translations on [Crowdin](https://translate.revanced.app).
|
||||||
|
|
||||||
## 🛠️ Building Manager from source
|
## 🛠️ Building Manager from source
|
||||||
|
|
||||||
1. Setup flutter environment for your [platform](https://docs.flutter.dev/get-started/install)
|
1. Setup flutter environment for your [platform](https://docs.flutter.dev/get-started/install)
|
||||||
2. Clone the repository locally
|
2. Clone the repository locally
|
||||||
3. Add your github token in gradle.properties like [this](/docs/4_building.md)
|
3. Add your GitHub token in gradle.properties like [this](/docs/4_building.md)
|
||||||
4. Open the project in terminal
|
4. Open the project in terminal
|
||||||
5. Run `flutter pub get` 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)
|
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`
|
7. To build release APK run `flutter build apk`
|
||||||
|
|||||||
@@ -85,10 +85,9 @@ dependencies {
|
|||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||||
|
|
||||||
// ReVanced
|
// ReVanced
|
||||||
implementation "app.revanced:revanced-patcher:11.0.4"
|
implementation "app.revanced:revanced-patcher:17.0.0"
|
||||||
|
|
||||||
// Signing & aligning
|
// Signing & aligning
|
||||||
implementation("org.bouncycastle:bcpkix-jdk15on:1.70")
|
implementation("org.bouncycastle:bcpkix-jdk15on:1.70")
|
||||||
implementation("com.android.tools.build:apksig:7.2.2")
|
implementation("com.android.tools.build:apksig:7.2.2")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,8 +25,7 @@
|
|||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:largeHeap="true"
|
android:largeHeap="true"
|
||||||
android:requestLegacyExternalStorage="true"
|
android:requestLegacyExternalStorage="true"
|
||||||
android:extractNativeLibs="true"
|
android:extractNativeLibs="true">
|
||||||
android:enableOnBackInvokedCallback="true">
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
@@ -43,6 +42,10 @@
|
|||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name=".ExportSettingsActivity"
|
||||||
|
android:exported="true">
|
||||||
|
</activity>
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="flutterEmbedding"
|
android:name="flutterEmbedding"
|
||||||
android:value="2" />
|
android:value="2" />
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
package app.revanced.manager.flutter
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageInfo
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Base64
|
||||||
|
import org.json.JSONObject
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
|
import java.io.File
|
||||||
|
import java.security.cert.CertificateFactory
|
||||||
|
import java.security.cert.X509Certificate
|
||||||
|
import java.security.MessageDigest
|
||||||
|
|
||||||
|
class ExportSettingsActivity : Activity() {
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
val callingPackageName = getCallingPackage()!!
|
||||||
|
|
||||||
|
if (getFingerprint(callingPackageName) == getFingerprint(getPackageName())) {
|
||||||
|
// Create JSON Object
|
||||||
|
val json = JSONObject()
|
||||||
|
|
||||||
|
// Default Data
|
||||||
|
json.put("keystorePassword", "s3cur3p@ssw0rd")
|
||||||
|
|
||||||
|
// Load Shared Preferences
|
||||||
|
val sharedPreferences = getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE)
|
||||||
|
val allEntries: Map<String, *> = sharedPreferences.getAll()
|
||||||
|
for ((key, value) in allEntries.entries) {
|
||||||
|
json.put(key.replace("flutter.", ""), value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load keystore
|
||||||
|
val keystoreFile = File(getExternalFilesDir(null), "/revanced-manager.keystore")
|
||||||
|
if (keystoreFile.exists()) {
|
||||||
|
val keystoreBytes = keystoreFile.readBytes()
|
||||||
|
val keystoreBase64 = Base64.encodeToString(keystoreBytes, Base64.DEFAULT)
|
||||||
|
json.put("keystore", keystoreBase64)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load saved patches
|
||||||
|
val storedPatchesFile = File(filesDir.parentFile.absolutePath, "/app_flutter/selected-patches.json")
|
||||||
|
if (storedPatchesFile.exists()) {
|
||||||
|
val patchesBytes = storedPatchesFile.readBytes()
|
||||||
|
val patches = String(patchesBytes, Charsets.UTF_8)
|
||||||
|
json.put("patches", JSONObject(patches))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send data back
|
||||||
|
val resultIntent = Intent()
|
||||||
|
resultIntent.putExtra("data", json.toString())
|
||||||
|
setResult(Activity.RESULT_OK, resultIntent)
|
||||||
|
finish()
|
||||||
|
} else {
|
||||||
|
val resultIntent = Intent()
|
||||||
|
setResult(Activity.RESULT_CANCELED)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getFingerprint(packageName: String): String {
|
||||||
|
// Get the signature of the app that matches the package name
|
||||||
|
val packageInfo = packageManager.getPackageInfo(packageName, PackageManager.GET_SIGNATURES)
|
||||||
|
val signature = packageInfo.signatures[0]
|
||||||
|
|
||||||
|
// Get the raw certificate data
|
||||||
|
val rawCert = signature.toByteArray()
|
||||||
|
|
||||||
|
// Generate an X509Certificate from the data
|
||||||
|
val certFactory = CertificateFactory.getInstance("X509")
|
||||||
|
val x509Cert = certFactory.generateCertificate(ByteArrayInputStream(rawCert)) as X509Certificate
|
||||||
|
|
||||||
|
// Get the SHA256 fingerprint
|
||||||
|
val fingerprint = MessageDigest.getInstance("SHA256").digest(x509Cert.encoded).joinToString("") {
|
||||||
|
"%02x".format(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fingerprint
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,28 +1,29 @@
|
|||||||
package app.revanced.manager.flutter
|
package app.revanced.manager.flutter
|
||||||
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import androidx.annotation.NonNull
|
|
||||||
import app.revanced.manager.flutter.utils.Aapt
|
import app.revanced.manager.flutter.utils.Aapt
|
||||||
import app.revanced.manager.flutter.utils.aligning.ZipAligner
|
import app.revanced.manager.flutter.utils.aligning.ZipAligner
|
||||||
import app.revanced.manager.flutter.utils.signing.Signer
|
import app.revanced.manager.flutter.utils.signing.Signer
|
||||||
import app.revanced.manager.flutter.utils.zip.ZipFile
|
import app.revanced.manager.flutter.utils.zip.ZipFile
|
||||||
import app.revanced.manager.flutter.utils.zip.structures.ZipEntry
|
import app.revanced.manager.flutter.utils.zip.structures.ZipEntry
|
||||||
|
import app.revanced.patcher.PatchBundleLoader
|
||||||
|
import app.revanced.patcher.PatchSet
|
||||||
import app.revanced.patcher.Patcher
|
import app.revanced.patcher.Patcher
|
||||||
import app.revanced.patcher.PatcherOptions
|
import app.revanced.patcher.PatcherOptions
|
||||||
import app.revanced.patcher.extensions.PatchExtensions.compatiblePackages
|
import app.revanced.patcher.patch.PatchResult
|
||||||
import app.revanced.patcher.extensions.PatchExtensions.patchName
|
|
||||||
import app.revanced.patcher.logging.Logger
|
|
||||||
import app.revanced.patcher.util.patch.PatchBundle
|
|
||||||
import dalvik.system.DexClassLoader
|
|
||||||
import io.flutter.embedding.android.FlutterActivity
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
import io.flutter.embedding.engine.FlutterEngine
|
import io.flutter.embedding.engine.FlutterEngine
|
||||||
import io.flutter.plugin.common.MethodChannel
|
import io.flutter.plugin.common.MethodChannel
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import org.json.JSONArray
|
||||||
|
import org.json.JSONObject
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.io.PrintWriter
|
||||||
private const val PATCHER_CHANNEL = "app.revanced.manager.flutter/patcher"
|
import java.io.StringWriter
|
||||||
private const val INSTALLER_CHANNEL = "app.revanced.manager.flutter/installer"
|
import java.util.logging.LogRecord
|
||||||
|
import java.util.logging.Logger
|
||||||
|
|
||||||
class MainActivity : FlutterActivity() {
|
class MainActivity : FlutterActivity() {
|
||||||
private val handler = Handler(Looper.getMainLooper())
|
private val handler = Handler(Looper.getMainLooper())
|
||||||
@@ -30,31 +31,42 @@ class MainActivity : FlutterActivity() {
|
|||||||
private var cancel: Boolean = false
|
private var cancel: Boolean = false
|
||||||
private var stopResult: MethodChannel.Result? = null
|
private var stopResult: MethodChannel.Result? = null
|
||||||
|
|
||||||
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
|
private lateinit var patches: PatchSet
|
||||||
|
|
||||||
|
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||||
super.configureFlutterEngine(flutterEngine)
|
super.configureFlutterEngine(flutterEngine)
|
||||||
val mainChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, PATCHER_CHANNEL)
|
|
||||||
installerChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, INSTALLER_CHANNEL)
|
val patcherChannel = "app.revanced.manager.flutter/patcher"
|
||||||
|
val installerChannel = "app.revanced.manager.flutter/installer"
|
||||||
|
|
||||||
|
val mainChannel =
|
||||||
|
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, patcherChannel)
|
||||||
|
|
||||||
|
this.installerChannel =
|
||||||
|
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, installerChannel)
|
||||||
|
|
||||||
mainChannel.setMethodCallHandler { call, result ->
|
mainChannel.setMethodCallHandler { call, result ->
|
||||||
when (call.method) {
|
when (call.method) {
|
||||||
"runPatcher" -> {
|
"runPatcher" -> {
|
||||||
val patchBundleFilePath = call.argument<String>("patchBundleFilePath")
|
|
||||||
val originalFilePath = call.argument<String>("originalFilePath")
|
val originalFilePath = call.argument<String>("originalFilePath")
|
||||||
val inputFilePath = call.argument<String>("inputFilePath")
|
val inputFilePath = call.argument<String>("inputFilePath")
|
||||||
val patchedFilePath = call.argument<String>("patchedFilePath")
|
val patchedFilePath = call.argument<String>("patchedFilePath")
|
||||||
val outFilePath = call.argument<String>("outFilePath")
|
val outFilePath = call.argument<String>("outFilePath")
|
||||||
val integrationsPath = call.argument<String>("integrationsPath")
|
val integrationsPath = call.argument<String>("integrationsPath")
|
||||||
val selectedPatches = call.argument<List<String>>("selectedPatches")
|
val selectedPatches = call.argument<List<String>>("selectedPatches")
|
||||||
|
val options = call.argument<Map<String, Map<String, Any>>>("options")
|
||||||
val cacheDirPath = call.argument<String>("cacheDirPath")
|
val cacheDirPath = call.argument<String>("cacheDirPath")
|
||||||
val keyStoreFilePath = call.argument<String>("keyStoreFilePath")
|
val keyStoreFilePath = call.argument<String>("keyStoreFilePath")
|
||||||
val keystorePassword = call.argument<String>("keystorePassword")
|
val keystorePassword = call.argument<String>("keystorePassword")
|
||||||
|
|
||||||
if (patchBundleFilePath != null &&
|
if (
|
||||||
originalFilePath != null &&
|
originalFilePath != null &&
|
||||||
inputFilePath != null &&
|
inputFilePath != null &&
|
||||||
patchedFilePath != null &&
|
patchedFilePath != null &&
|
||||||
outFilePath != null &&
|
outFilePath != null &&
|
||||||
integrationsPath != null &&
|
integrationsPath != null &&
|
||||||
selectedPatches != null &&
|
selectedPatches != null &&
|
||||||
|
options != null &&
|
||||||
cacheDirPath != null &&
|
cacheDirPath != null &&
|
||||||
keyStoreFilePath != null &&
|
keyStoreFilePath != null &&
|
||||||
keystorePassword != null
|
keystorePassword != null
|
||||||
@@ -62,20 +74,18 @@ class MainActivity : FlutterActivity() {
|
|||||||
cancel = false
|
cancel = false
|
||||||
runPatcher(
|
runPatcher(
|
||||||
result,
|
result,
|
||||||
patchBundleFilePath,
|
|
||||||
originalFilePath,
|
originalFilePath,
|
||||||
inputFilePath,
|
inputFilePath,
|
||||||
patchedFilePath,
|
patchedFilePath,
|
||||||
outFilePath,
|
outFilePath,
|
||||||
integrationsPath,
|
integrationsPath,
|
||||||
selectedPatches,
|
selectedPatches,
|
||||||
|
options,
|
||||||
cacheDirPath,
|
cacheDirPath,
|
||||||
keyStoreFilePath,
|
keyStoreFilePath,
|
||||||
keystorePassword
|
keystorePassword
|
||||||
)
|
)
|
||||||
} else {
|
} else result.notImplemented()
|
||||||
result.notImplemented()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
"stopPatcher" -> {
|
"stopPatcher" -> {
|
||||||
@@ -83,6 +93,69 @@ class MainActivity : FlutterActivity() {
|
|||||||
stopResult = result
|
stopResult = result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"getPatches" -> {
|
||||||
|
val patchBundleFilePath = call.argument<String>("patchBundleFilePath")!!
|
||||||
|
val cacheDirPath = call.argument<String>("cacheDirPath")!!
|
||||||
|
|
||||||
|
try {
|
||||||
|
patches = PatchBundleLoader.Dex(
|
||||||
|
File(patchBundleFilePath),
|
||||||
|
optimizedDexDirectory = File(cacheDirPath)
|
||||||
|
)
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
return@setMethodCallHandler result.notImplemented()
|
||||||
|
} catch (err: Error) {
|
||||||
|
return@setMethodCallHandler result.notImplemented()
|
||||||
|
}
|
||||||
|
|
||||||
|
JSONArray().apply {
|
||||||
|
patches.forEach {
|
||||||
|
JSONObject().apply {
|
||||||
|
put("name", it.name)
|
||||||
|
put("description", it.description)
|
||||||
|
put("excluded", !it.use)
|
||||||
|
put("compatiblePackages", JSONArray().apply {
|
||||||
|
it.compatiblePackages?.forEach { compatiblePackage ->
|
||||||
|
val compatiblePackageJson = JSONObject().apply {
|
||||||
|
put("name", compatiblePackage.name)
|
||||||
|
put(
|
||||||
|
"versions",
|
||||||
|
JSONArray().apply {
|
||||||
|
compatiblePackage.versions?.forEach { version ->
|
||||||
|
put(version)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
put(compatiblePackageJson)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
put("options", JSONArray().apply {
|
||||||
|
it.options.values.forEach { option ->
|
||||||
|
val optionJson = JSONObject().apply option@{
|
||||||
|
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 {
|
||||||
|
|
||||||
|
value.forEach { put(it) }
|
||||||
|
})
|
||||||
|
else -> put("value", option.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
put("optionClassType", option::class.simpleName)
|
||||||
|
}
|
||||||
|
put(optionJson)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}.let(::put)
|
||||||
|
}
|
||||||
|
}.toString().let(result::success)
|
||||||
|
}
|
||||||
|
|
||||||
else -> result.notImplemented()
|
else -> result.notImplemented()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -90,13 +163,13 @@ class MainActivity : FlutterActivity() {
|
|||||||
|
|
||||||
private fun runPatcher(
|
private fun runPatcher(
|
||||||
result: MethodChannel.Result,
|
result: MethodChannel.Result,
|
||||||
patchBundleFilePath: String,
|
|
||||||
originalFilePath: String,
|
originalFilePath: String,
|
||||||
inputFilePath: String,
|
inputFilePath: String,
|
||||||
patchedFilePath: String,
|
patchedFilePath: String,
|
||||||
outFilePath: String,
|
outFilePath: String,
|
||||||
integrationsPath: String,
|
integrationsPath: String,
|
||||||
selectedPatches: List<String>,
|
selectedPatches: List<String>,
|
||||||
|
options: Map<String, Map<String, Any>>,
|
||||||
cacheDirPath: String,
|
cacheDirPath: String,
|
||||||
keyStoreFilePath: String,
|
keyStoreFilePath: String,
|
||||||
keystorePassword: String
|
keystorePassword: String
|
||||||
@@ -107,179 +180,146 @@ class MainActivity : FlutterActivity() {
|
|||||||
val outFile = File(outFilePath)
|
val outFile = File(outFilePath)
|
||||||
val integrations = File(integrationsPath)
|
val integrations = File(integrationsPath)
|
||||||
val keyStoreFile = File(keyStoreFilePath)
|
val keyStoreFile = File(keyStoreFilePath)
|
||||||
|
val cacheDir = File(cacheDirPath)
|
||||||
|
|
||||||
Thread {
|
Thread {
|
||||||
try {
|
fun updateProgress(progress: Double, header: String, log: String) {
|
||||||
handler.post {
|
handler.post {
|
||||||
installerChannel.invokeMethod(
|
installerChannel.invokeMethod(
|
||||||
"update",
|
"update",
|
||||||
mapOf(
|
mapOf(
|
||||||
"progress" to 0.1,
|
"progress" to progress,
|
||||||
"header" to "",
|
"header" to header,
|
||||||
"log" to "Copying original APK"
|
"log" to log
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if(cancel) {
|
fun postStop() = handler.post { stopResult!!.success(null) }
|
||||||
handler.post { stopResult!!.success(null) }
|
|
||||||
|
// Setup logger
|
||||||
|
Logger.getLogger("").apply {
|
||||||
|
handlers.forEach {
|
||||||
|
it.close()
|
||||||
|
removeHandler(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
object : java.util.logging.Handler() {
|
||||||
|
override fun publish(record: LogRecord) {
|
||||||
|
if (record.loggerName?.startsWith("app.revanced") != true || cancel) return
|
||||||
|
|
||||||
|
updateProgress(-1.0, "", record.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun flush() = Unit
|
||||||
|
override fun close() = flush()
|
||||||
|
}.let(::addHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
updateProgress(0.0, "", "Copying APK")
|
||||||
|
|
||||||
|
if (cancel) {
|
||||||
|
postStop()
|
||||||
return@Thread
|
return@Thread
|
||||||
}
|
}
|
||||||
|
|
||||||
originalFile.copyTo(inputFile, true)
|
originalFile.copyTo(inputFile, true)
|
||||||
|
|
||||||
handler.post {
|
if (cancel) {
|
||||||
installerChannel.invokeMethod(
|
postStop()
|
||||||
"update",
|
|
||||||
mapOf(
|
|
||||||
"progress" to 0.2,
|
|
||||||
"header" to "Reading APK...",
|
|
||||||
"log" to "Reading input APK"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if(cancel) {
|
|
||||||
handler.post { stopResult!!.success(null) }
|
|
||||||
return@Thread
|
return@Thread
|
||||||
}
|
}
|
||||||
|
|
||||||
val patcher =
|
updateProgress(0.05, "Reading APK...", "Reading APK")
|
||||||
Patcher(
|
|
||||||
PatcherOptions(
|
val patcher = Patcher(
|
||||||
inputFile,
|
PatcherOptions(
|
||||||
cacheDirPath,
|
inputFile,
|
||||||
Aapt.binary(applicationContext).absolutePath,
|
cacheDir,
|
||||||
cacheDirPath,
|
Aapt.binary(applicationContext).absolutePath,
|
||||||
logger = ManagerLogger()
|
cacheDir.path,
|
||||||
)
|
true // TODO: Add option to disable this
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
if (cancel) {
|
if (cancel) {
|
||||||
handler.post { stopResult!!.success(null) }
|
postStop()
|
||||||
return@Thread
|
return@Thread
|
||||||
}
|
}
|
||||||
|
|
||||||
handler.post {
|
updateProgress(0.1, "Loading patches...", "Loading patches")
|
||||||
installerChannel.invokeMethod(
|
|
||||||
"update",
|
|
||||||
mapOf("progress" to 0.3, "header" to "", "log" to "")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
handler.post {
|
|
||||||
installerChannel.invokeMethod(
|
|
||||||
"update",
|
|
||||||
mapOf(
|
|
||||||
"progress" to 0.4,
|
|
||||||
"header" to "Merging integrations...",
|
|
||||||
"log" to "Merging integrations"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if(cancel) {
|
val patches = patches.filter { patch ->
|
||||||
handler.post { stopResult!!.success(null) }
|
val isCompatible = patch.compatiblePackages?.any {
|
||||||
return@Thread
|
it.name == patcher.context.packageMetadata.packageName
|
||||||
}
|
} ?: false
|
||||||
|
|
||||||
patcher.addIntegrations(listOf(integrations)) {}
|
val compatibleOrUniversal =
|
||||||
|
isCompatible || patch.compatiblePackages.isNullOrEmpty()
|
||||||
|
|
||||||
if(cancel) {
|
compatibleOrUniversal && selectedPatches.any { it == patch.name }
|
||||||
handler.post { stopResult!!.success(null) }
|
}.onEach { patch ->
|
||||||
return@Thread
|
options[patch.name]?.forEach { (key, value) ->
|
||||||
}
|
patch.options[key] = value
|
||||||
|
|
||||||
handler.post {
|
|
||||||
installerChannel.invokeMethod(
|
|
||||||
"update",
|
|
||||||
mapOf(
|
|
||||||
"progress" to 0.5,
|
|
||||||
"header" to "Applying patches...",
|
|
||||||
"log" to ""
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if(cancel) {
|
|
||||||
handler.post { stopResult!!.success(null) }
|
|
||||||
return@Thread
|
|
||||||
}
|
|
||||||
|
|
||||||
val patches = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.CUPCAKE) {
|
|
||||||
PatchBundle.Dex(
|
|
||||||
patchBundleFilePath,
|
|
||||||
DexClassLoader(
|
|
||||||
patchBundleFilePath,
|
|
||||||
cacheDirPath,
|
|
||||||
null,
|
|
||||||
javaClass.classLoader
|
|
||||||
)
|
|
||||||
).loadPatches().filter { patch ->
|
|
||||||
(patch.compatiblePackages?.any { it.name == patcher.context.packageMetadata.packageName } == true || patch.compatiblePackages.isNullOrEmpty()) &&
|
|
||||||
selectedPatches.any { it == patch.patchName }
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
TODO("VERSION.SDK_INT < CUPCAKE")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if(cancel) {
|
if (cancel) {
|
||||||
handler.post { stopResult!!.success(null) }
|
postStop()
|
||||||
return@Thread
|
return@Thread
|
||||||
}
|
}
|
||||||
|
|
||||||
patcher.addPatches(patches)
|
updateProgress(0.15, "Executing...", "")
|
||||||
patcher.executePatches().forEach { (patch, res) ->
|
|
||||||
if (res.isSuccess) {
|
// Update the progress bar every time a patch is executed from 0.15 to 0.7
|
||||||
val msg = "Applied $patch"
|
val totalPatchesCount = patches.size
|
||||||
handler.post {
|
val progressStep = 0.55 / totalPatchesCount
|
||||||
installerChannel.invokeMethod(
|
var progress = 0.15
|
||||||
"update",
|
|
||||||
mapOf(
|
patcher.apply {
|
||||||
"progress" to 0.5,
|
acceptIntegrations(listOf(integrations))
|
||||||
"header" to "",
|
acceptPatches(patches)
|
||||||
"log" to msg
|
|
||||||
)
|
runBlocking {
|
||||||
)
|
apply(false).collect { patchResult: PatchResult ->
|
||||||
|
if (cancel) {
|
||||||
|
handler.post { stopResult!!.success(null) }
|
||||||
|
this.cancel()
|
||||||
|
this@apply.close()
|
||||||
|
return@collect
|
||||||
|
}
|
||||||
|
|
||||||
|
val msg = patchResult.exception?.let {
|
||||||
|
val writer = StringWriter()
|
||||||
|
it.printStackTrace(PrintWriter(writer))
|
||||||
|
"${patchResult.patch.name} failed: $writer"
|
||||||
|
} ?: run {
|
||||||
|
"${patchResult.patch.name} succeeded"
|
||||||
|
}
|
||||||
|
|
||||||
|
updateProgress(progress, "", msg)
|
||||||
|
progress += progressStep
|
||||||
}
|
}
|
||||||
if(cancel) {
|
|
||||||
handler.post { stopResult!!.success(null) }
|
|
||||||
return@Thread
|
|
||||||
}
|
|
||||||
return@forEach
|
|
||||||
}
|
|
||||||
val msg =
|
|
||||||
"Failed to apply $patch: " + "${res.exceptionOrNull()!!.message ?: res.exceptionOrNull()!!.cause!!::class.simpleName}"
|
|
||||||
handler.post {
|
|
||||||
installerChannel.invokeMethod(
|
|
||||||
"update",
|
|
||||||
mapOf("progress" to 0.5, "header" to "", "log" to msg)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if(cancel) {
|
|
||||||
handler.post { stopResult!!.success(null) }
|
|
||||||
return@Thread
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handler.post {
|
if (cancel) {
|
||||||
installerChannel.invokeMethod(
|
postStop()
|
||||||
"update",
|
patcher.close()
|
||||||
mapOf(
|
|
||||||
"progress" to 0.7,
|
|
||||||
"header" to "Repacking APK...",
|
|
||||||
"log" to ""
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if(cancel) {
|
|
||||||
handler.post { stopResult!!.success(null) }
|
|
||||||
return@Thread
|
return@Thread
|
||||||
}
|
}
|
||||||
val res = patcher.save()
|
|
||||||
|
updateProgress(0.8, "Building...", "")
|
||||||
|
|
||||||
|
val res = patcher.get()
|
||||||
|
patcher.close()
|
||||||
|
|
||||||
ZipFile(patchedFile).use { file ->
|
ZipFile(patchedFile).use { file ->
|
||||||
res.dexFiles.forEach {
|
res.dexFiles.forEach {
|
||||||
if (cancel) {
|
if (cancel) {
|
||||||
handler.post { stopResult!!.success(null) }
|
postStop()
|
||||||
return@Thread
|
return@Thread
|
||||||
}
|
}
|
||||||
file.addEntryCompressData(
|
file.addEntryCompressData(
|
||||||
@@ -298,90 +338,35 @@ class MainActivity : FlutterActivity() {
|
|||||||
ZipAligner::getEntryAlignment
|
ZipAligner::getEntryAlignment
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cancel) {
|
if (cancel) {
|
||||||
handler.post { stopResult!!.success(null) }
|
postStop()
|
||||||
return@Thread
|
return@Thread
|
||||||
}
|
}
|
||||||
handler.post {
|
|
||||||
installerChannel.invokeMethod(
|
updateProgress(0.9, "Signing...", "Signing APK")
|
||||||
"update",
|
|
||||||
mapOf(
|
|
||||||
"progress" to 0.9,
|
|
||||||
"header" to "Signing APK...",
|
|
||||||
"log" to ""
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Signer("ReVanced", keystorePassword).signApk(
|
Signer("ReVanced", keystorePassword)
|
||||||
patchedFile,
|
.signApk(patchedFile, outFile, keyStoreFile)
|
||||||
outFile,
|
|
||||||
keyStoreFile
|
|
||||||
)
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
//log to console
|
|
||||||
print("Error signing APK: ${e.message}")
|
print("Error signing APK: ${e.message}")
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
|
|
||||||
handler.post {
|
updateProgress(1.0, "Patched", "Patched")
|
||||||
installerChannel.invokeMethod(
|
|
||||||
"update",
|
|
||||||
mapOf(
|
|
||||||
"progress" to 1.0,
|
|
||||||
"header" to "Finished!",
|
|
||||||
"log" to "Finished!"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} catch (ex: Throwable) {
|
} catch (ex: Throwable) {
|
||||||
val stack = ex.stackTraceToString()
|
if (!cancel) {
|
||||||
handler.post {
|
val stack = ex.stackTraceToString()
|
||||||
installerChannel.invokeMethod(
|
updateProgress(
|
||||||
"update",
|
-100.0,
|
||||||
mapOf(
|
"Failed",
|
||||||
"progress" to -100.0,
|
"An error occurred:\n$stack"
|
||||||
"header" to "Aborted...",
|
|
||||||
"log" to "An error occurred! Aborted\nError:\n$stack"
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handler.post { result.success(null) }
|
handler.post { result.success(null) }
|
||||||
}.start()
|
}.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class ManagerLogger : Logger {
|
|
||||||
override fun error(msg: String) {
|
|
||||||
handler.post {
|
|
||||||
installerChannel
|
|
||||||
.invokeMethod(
|
|
||||||
"update",
|
|
||||||
mapOf("progress" to -1.0, "header" to "", "log" to msg)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun warn(msg: String) {
|
|
||||||
handler.post {
|
|
||||||
installerChannel.invokeMethod(
|
|
||||||
"update",
|
|
||||||
mapOf("progress" to -1.0, "header" to "", "log" to msg)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun info(msg: String) {
|
|
||||||
handler.post {
|
|
||||||
installerChannel.invokeMethod(
|
|
||||||
"update",
|
|
||||||
mapOf("progress" to -1.0, "header" to "", "log" to msg)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun trace(_msg: String) { /* unused */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
buildscript {
|
buildscript {
|
||||||
ext.kotlin_version = '1.7.10'
|
ext.kotlin_version = '1.9.0'
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
@@ -16,11 +16,7 @@ allprojects {
|
|||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
maven {
|
maven {
|
||||||
url = uri("https://maven.pkg.github.com/revanced/revanced-patcher")
|
url 'https://jitpack.io'
|
||||||
credentials {
|
|
||||||
username = (project.findProperty("gpr.user") ?: System.getenv("GITHUB_ACTOR")) as String
|
|
||||||
password = (project.findProperty("gpr.key") ?: System.getenv("GITHUB_TOKEN")) as String
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
mavenLocal()
|
mavenLocal()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionSha256Sum=a01b6587e15fe7ed120a0ee299c25982a1eee045abd6a9dd5e216b2f628ef9ac
|
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.2-bin.zip
|
|
||||||
networkTimeout=10000
|
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip
|
||||||
|
|||||||
@@ -10,9 +10,11 @@
|
|||||||
"yesButton": "Yes",
|
"yesButton": "Yes",
|
||||||
"noButton": "No",
|
"noButton": "No",
|
||||||
"warning": "Warning",
|
"warning": "Warning",
|
||||||
|
"options": "Options",
|
||||||
"notice": "Notice",
|
"notice": "Notice",
|
||||||
"noShowAgain": "Don't show this again",
|
"noShowAgain": "Don't show this again",
|
||||||
"new": "New",
|
"add": "Add",
|
||||||
|
"remove": "Remove",
|
||||||
"navigationView": {
|
"navigationView": {
|
||||||
"dashboardTab": "Dashboard",
|
"dashboardTab": "Dashboard",
|
||||||
"patcherTab": "Patcher",
|
"patcherTab": "Patcher",
|
||||||
@@ -23,13 +25,13 @@
|
|||||||
"widgetTitle": "Dashboard",
|
"widgetTitle": "Dashboard",
|
||||||
|
|
||||||
"updatesSubtitle": "Updates",
|
"updatesSubtitle": "Updates",
|
||||||
"patchedSubtitle": "Patched applications",
|
"patchedSubtitle": "Patched apps",
|
||||||
|
|
||||||
"noUpdates": "No updates available",
|
"noUpdates": "No updates available",
|
||||||
|
|
||||||
"WIP": "Work in progress...",
|
"WIP": "Work in progress...",
|
||||||
|
|
||||||
"noInstallations": "No patched applications installed",
|
"noInstallations": "No patched apps installed",
|
||||||
"installUpdate": "Continue to install the update?",
|
"installUpdate": "Continue to install the update?",
|
||||||
|
|
||||||
"updateDialogTitle": "Update Manager",
|
"updateDialogTitle": "Update Manager",
|
||||||
@@ -56,9 +58,7 @@
|
|||||||
"updatesDisabled": "Updating a patched app is currently disabled. Repatch the app again."
|
"updatesDisabled": "Updating a patched app is currently disabled. Repatch the app again."
|
||||||
},
|
},
|
||||||
"applicationItem": {
|
"applicationItem": {
|
||||||
"patchButton": "Patch",
|
"infoButton": "Info"
|
||||||
"infoButton": "Info",
|
|
||||||
"changelogLabel": "Changelog"
|
|
||||||
},
|
},
|
||||||
"latestCommitCard": {
|
"latestCommitCard": {
|
||||||
"loadingLabel": "Loading...",
|
"loadingLabel": "Loading...",
|
||||||
@@ -71,10 +71,10 @@
|
|||||||
"widgetTitle": "Patcher",
|
"widgetTitle": "Patcher",
|
||||||
"patchButton": "Patch",
|
"patchButton": "Patch",
|
||||||
|
|
||||||
"patchDialogText": "You have selected a resource patch and a split APK installation has been detected, so patching errors may occur.\nAre you sure you want to proceed?",
|
|
||||||
"armv7WarningDialogText": "Patching on ARMv7 devices is not yet supported and might fail. Proceed anyways?",
|
"armv7WarningDialogText": "Patching on ARMv7 devices is not yet supported and might fail. Proceed anyways?",
|
||||||
"splitApkWarningDialogText": "Patching a split APK is not yet supported and might fail. Proceed anyways?",
|
|
||||||
"removedPatchesWarningDialogText": "The following patches have been removed since the last time you used them.\n\n{patches}\n\nProceed anyways?"
|
"removedPatchesWarningDialogText": "The following patches have been removed since the last time you used them.\n\n{patches}\n\nProceed anyways?",
|
||||||
|
"requiredOptionDialogText" : "Some patch options have to be set."
|
||||||
},
|
},
|
||||||
"appSelectorCard": {
|
"appSelectorCard": {
|
||||||
"widgetTitle": "Select an application",
|
"widgetTitle": "Select an application",
|
||||||
@@ -117,6 +117,8 @@
|
|||||||
"viewTitle": "Select patches",
|
"viewTitle": "Select patches",
|
||||||
"searchBarHint": "Search patches",
|
"searchBarHint": "Search patches",
|
||||||
"universalPatches": "Universal patches",
|
"universalPatches": "Universal patches",
|
||||||
|
"newPatches": "New patches",
|
||||||
|
"patches": "Patches",
|
||||||
|
|
||||||
"doneButton": "Done",
|
"doneButton": "Done",
|
||||||
|
|
||||||
@@ -126,20 +128,32 @@
|
|||||||
"none": "None",
|
"none": "None",
|
||||||
"noneTooltip": "Deselect all patches",
|
"noneTooltip": "Deselect all patches",
|
||||||
|
|
||||||
"loadPatchesSelection": "Load patches selection",
|
"loadPatchesSelection": "Load patch selection",
|
||||||
"noSavedPatches": "No saved patches for the selected app.\nPress Done to save current 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",
|
"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."
|
||||||
|
},
|
||||||
|
"patchOptionsView": {
|
||||||
|
"viewTitle": "Patch options",
|
||||||
|
"saveOptions": "Save",
|
||||||
|
|
||||||
"selectAllPatchesWarningContent": "You are about to select all patches, that includes non-suggested patches and can cause unwanted behavior."
|
"addOptions": "Add options",
|
||||||
|
"deselectPatch": "Deselect patch",
|
||||||
|
"tooltip": "More input options",
|
||||||
|
"selectFilePath": "Select file path",
|
||||||
|
"selectFolder": "Select folder",
|
||||||
|
"selectOption": "Select option",
|
||||||
|
|
||||||
|
"requiredOption": "This option is required",
|
||||||
|
"unsupportedOption": "This option is not supported",
|
||||||
|
"requiredOptionNull": "The following options have to be set:\n\n{options}"
|
||||||
},
|
},
|
||||||
"patchItem": {
|
"patchItem": {
|
||||||
"unsupportedDialogText": "Selecting this patch may result in patching errors.\n\nApp version: {packageVersion}\nSupported versions:\n{supportedVersions}",
|
"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",
|
||||||
|
|
||||||
"newPatchDialogText": "This is a new patch that has been added since the last time you have patched this app.",
|
"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.",
|
||||||
"newPatch": "New patch",
|
|
||||||
|
|
||||||
"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.",
|
|
||||||
"patchesChangeWarningDialogButton": "Use default selection"
|
"patchesChangeWarningDialogButton": "Use default selection"
|
||||||
},
|
},
|
||||||
"installerView": {
|
"installerView": {
|
||||||
@@ -148,9 +162,8 @@
|
|||||||
"installTypeDescription": "Select the installation type to proceed with.",
|
"installTypeDescription": "Select the installation type to proceed with.",
|
||||||
|
|
||||||
"installButton": "Install",
|
"installButton": "Install",
|
||||||
"installRootType": "Root",
|
"installRootType": "Mount",
|
||||||
"installNonRootType": "Non-root",
|
"installNonRootType": "Normal",
|
||||||
"installRecommendedType": "Recommended",
|
|
||||||
|
|
||||||
"pressBackAgain": "Press back again to cancel",
|
"pressBackAgain": "Press back again to cancel",
|
||||||
"openButton": "Open",
|
"openButton": "Open",
|
||||||
@@ -162,10 +175,9 @@
|
|||||||
"exportApkButtonTooltip": "Export patched APK",
|
"exportApkButtonTooltip": "Export patched APK",
|
||||||
"exportLogButtonTooltip": "Export log",
|
"exportLogButtonTooltip": "Export log",
|
||||||
|
|
||||||
"installErrorDialogTitle": "Error",
|
"screenshotDetected": "A screenshot has been detected. If you are trying to share the log, please share a text copy instead.\n\nCopy log to clipboard?",
|
||||||
"installErrorDialogText1": "Root install is not possible with the current patches selection.\nRepatch your app or choose non-root install.",
|
"copiedToClipboard": "Copied log to clipboard",
|
||||||
"installErrorDialogText2": "Non-root install is not possible with the current patches selection.\nRepatch your app or choose root install if you have your device rooted.",
|
|
||||||
"installErrorDialogText3": "Root install is not possible as the original APK was selected from storage.\nSelect an installed app or choose non-root install.",
|
|
||||||
"noExit": "Installer is still running, cannot exit..."
|
"noExit": "Installer is still running, cannot exit..."
|
||||||
},
|
},
|
||||||
"settingsView": {
|
"settingsView": {
|
||||||
@@ -173,13 +185,14 @@
|
|||||||
|
|
||||||
"appearanceSectionTitle": "Appearance",
|
"appearanceSectionTitle": "Appearance",
|
||||||
"teamSectionTitle": "Team",
|
"teamSectionTitle": "Team",
|
||||||
"infoSectionTitle": "Info",
|
"debugSectionTitle": "Debugging",
|
||||||
"advancedSectionTitle": "Advanced",
|
"advancedSectionTitle": "Advanced",
|
||||||
"exportSectionTitle": "Import & export",
|
"exportSectionTitle": "Import & export",
|
||||||
"logsSectionTitle": "Logs",
|
|
||||||
|
|
||||||
"darkThemeLabel": "Dark mode",
|
"themeModeLabel": "App theme",
|
||||||
"darkThemeHint": "Welcome to the dark side",
|
"systemThemeLabel": "System",
|
||||||
|
"lightThemeLabel": "Light",
|
||||||
|
"darkThemeLabel": "Dark",
|
||||||
|
|
||||||
"dynamicThemeLabel": "Material You",
|
"dynamicThemeLabel": "Material You",
|
||||||
"dynamicThemeHint": "Enjoy an experience closer to your device",
|
"dynamicThemeHint": "Enjoy an experience closer to your device",
|
||||||
@@ -188,15 +201,15 @@
|
|||||||
"englishOption": "English",
|
"englishOption": "English",
|
||||||
|
|
||||||
"sourcesLabel": "Sources",
|
"sourcesLabel": "Sources",
|
||||||
"sourcesLabelHint": "Configure your custom sources",
|
"sourcesLabelHint": "Configure your sources",
|
||||||
"sourcesIntegrationsLabel": "Integrations source",
|
"sourcesIntegrationsLabel": "Integrations source",
|
||||||
"sourcesResetDialogTitle": "Reset",
|
"sourcesResetDialogTitle": "Reset",
|
||||||
"sourcesResetDialogText": "Are you sure you want to reset custom sources to their default values?",
|
"sourcesResetDialogText": "Are you sure you want to reset your sources to their default values?",
|
||||||
"apiURLResetDialogText": "Are you sure you want to reset API URL to its default value?",
|
"apiURLResetDialogText": "Are you sure you want to reset your 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.",
|
"sourcesUpdateNote": "Note: Patches will be updated to the latest version automatically.\n\nThis will reveal your IP address to the server.",
|
||||||
|
|
||||||
"apiURLLabel": "API URL",
|
"apiURLLabel": "API URL",
|
||||||
"apiURLHint": "Configure your custom API URL",
|
"apiURLHint": "Configure your API URL",
|
||||||
"selectApiURL": "API URL",
|
"selectApiURL": "API URL",
|
||||||
"hostRepositoryLabel": "Repository API",
|
"hostRepositoryLabel": "Repository API",
|
||||||
"orgPatchesLabel": "Patches organization",
|
"orgPatchesLabel": "Patches organization",
|
||||||
@@ -206,21 +219,20 @@
|
|||||||
"contributorsLabel": "Contributors",
|
"contributorsLabel": "Contributors",
|
||||||
"contributorsHint": "A list of contributors of ReVanced",
|
"contributorsHint": "A list of contributors of ReVanced",
|
||||||
|
|
||||||
"logsLabel": "Logs",
|
"logsLabel": "Share logs",
|
||||||
"logsHint": "Share Manager's logs",
|
"logsHint": "Share ReVanced Manager logs",
|
||||||
|
|
||||||
"enablePatchesSelectionLabel": "Enable changing selection",
|
"enablePatchesSelectionLabel": "Allow changing patch selection",
|
||||||
"enablePatchesSelectionHint": "Enable changing the selection of patches.",
|
"enablePatchesSelectionHint": "Allow changing the selection of patches",
|
||||||
"enablePatchesSelectionWarningText": "Changing the default selection of patches may cause unexpected issues.\n\nEnable anyways?",
|
"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?",
|
"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",
|
"autoUpdatePatchesLabel": "Auto update patches",
|
||||||
"autoUpdatePatchesHint": "Automatically update ReVanced Patches to the latest version",
|
"autoUpdatePatchesHint": "Automatically update patches to the latest version",
|
||||||
"experimentalUniversalPatchesLabel": "Experimental universal patches support",
|
"universalPatchesLabel": "Show universal patches",
|
||||||
"experimentalUniversalPatchesHint": "Display all applications to use with universal patches, loading list of apps may be slower",
|
"universalPatchesHint": "Display all apps and universal patches (may slow down the app list)",
|
||||||
"experimentalPatchesLabel": "Experimental patches support",
|
"versionCompatibilityCheckLabel": "Version compatibility check",
|
||||||
"experimentalPatchesHint": "Enable usage of unsupported patches in any app version",
|
"versionCompatibilityCheckHint": "Restricts patches to supported versions",
|
||||||
"enabledExperimentalPatches": "Experimental patches support enabled",
|
|
||||||
|
|
||||||
"aboutLabel": "About",
|
"aboutLabel": "About",
|
||||||
"snackbarMessage": "Copied to clipboard",
|
"snackbarMessage": "Copied to clipboard",
|
||||||
@@ -230,47 +242,52 @@
|
|||||||
"deleteTempDirHint": "Delete unused temporary files",
|
"deleteTempDirHint": "Delete unused temporary files",
|
||||||
"deletedTempDir": "Temporary files deleted",
|
"deletedTempDir": "Temporary files deleted",
|
||||||
|
|
||||||
"exportPatchesLabel": "Export patches selection",
|
"exportPatchesLabel": "Export patch selection",
|
||||||
"exportPatchesHint": "Export patches selection to a JSON file",
|
"exportPatchesHint": "Export patch selection to a JSON file",
|
||||||
"exportedPatches": "Patches selection exported",
|
"exportedPatches": "Patch selection exported",
|
||||||
"noExportFileFound": "No patches selection to export",
|
"noExportFileFound": "No patch selection to export",
|
||||||
|
|
||||||
"importPatchesLabel": "Import patches selection",
|
"importPatchesLabel": "Import patch selection",
|
||||||
"importPatchesHint": "Import patches selection from a JSON file",
|
"importPatchesHint": "Import patch selection from a JSON file",
|
||||||
"importedPatches": "Patches selection imported",
|
"importedPatches": "Patch selection imported",
|
||||||
|
|
||||||
"resetStoredPatchesLabel": "Reset patches",
|
"resetStoredPatchesLabel": "Reset patch selection",
|
||||||
"resetStoredPatchesHint": "Reset the stored patches 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",
|
||||||
|
|
||||||
"resetStoredPatchesDialogTitle": "Reset patches selection?",
|
"resetStoredOptionsLabel": "Reset patch options",
|
||||||
"resetStoredPatchesDialogText": "Resetting patches selection will remove all selected patches.",
|
"resetStoredOptionsHint": "Reset all patch options",
|
||||||
"resetStoredPatches": "Patches selection has been reset",
|
"resetStoredOptionsDialogTitle": "Reset patch options?",
|
||||||
|
"resetStoredOptionsDialogText": "Resetting patch options will remove all saved options.",
|
||||||
|
"resetStoredOptions": "Options have been reset",
|
||||||
|
|
||||||
"deleteLogsLabel": "Delete logs",
|
"deleteLogsLabel": "Clear logs",
|
||||||
"deleteLogsHint": "Delete collected manager logs",
|
"deleteLogsHint": "Delete collected ReVanced Manager logs",
|
||||||
"deletedLogs": "Logs deleted",
|
"deletedLogs": "Logs deleted",
|
||||||
|
|
||||||
"regenerateKeystoreLabel": "Regenerate keystore",
|
"regenerateKeystoreLabel": "Regenerate keystore",
|
||||||
"regenerateKeystoreHint": "Regenerate the keystore used to sign the app",
|
"regenerateKeystoreHint": "Regenerate the keystore used to sign apps",
|
||||||
|
|
||||||
"regenerateKeystoreDialogTitle": "Regenerate keystore?",
|
"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",
|
"regeneratedKeystore": "Keystore regenerated",
|
||||||
|
|
||||||
"exportKeystoreLabel": "Export keystore",
|
"exportKeystoreLabel": "Export keystore",
|
||||||
"exportKeystoreHint": "Export keystore used to sign apps",
|
"exportKeystoreHint": "Export the keystore used to sign apps",
|
||||||
"exportedKeystore": "Keystore exported",
|
"exportedKeystore": "Keystore exported",
|
||||||
"noKeystoreExportFileFound": "No keystore to export",
|
"noKeystoreExportFileFound": "No keystore to export",
|
||||||
|
|
||||||
"importKeystoreLabel": "Import keystore",
|
"importKeystoreLabel": "Import keystore",
|
||||||
"importKeystoreHint": "Import keystore used to sign apps",
|
"importKeystoreHint": "Import a keystore used to sign apps",
|
||||||
"importedKeystore": "Keystore imported",
|
"importedKeystore": "Keystore imported",
|
||||||
|
|
||||||
"selectKeystorePassword": "Keystore Password",
|
"selectKeystorePassword": "Keystore password",
|
||||||
"selectKeystorePasswordHint": "Select keystore password used to sign the apk",
|
"selectKeystorePasswordHint": "Select keystore password used to sign apps",
|
||||||
|
|
||||||
"jsonSelectorErrorMessage": "Unable to use selected JSON file",
|
"jsonSelectorErrorMessage": "Unable to use selected JSON file",
|
||||||
"keystoreSelectorErrorMessage": "Unable to use selected KEYSTORE file"
|
"keystoreSelectorErrorMessage": "Unable to use selected keystore file"
|
||||||
},
|
},
|
||||||
"appInfoView": {
|
"appInfoView": {
|
||||||
"widgetTitle": "App info",
|
"widgetTitle": "App info",
|
||||||
@@ -283,7 +300,6 @@
|
|||||||
"rootDialogText": "App was installed with superuser permissions, but currently ReVanced Manager has no permissions.\nPlease grant superuser permissions first.",
|
"rootDialogText": "App was installed with superuser permissions, but currently ReVanced Manager has no permissions.\nPlease grant superuser permissions first.",
|
||||||
|
|
||||||
"packageNameLabel": "Package name",
|
"packageNameLabel": "Package name",
|
||||||
"originalPackageNameLabel": "Original package name",
|
|
||||||
"installTypeLabel": "Installation type",
|
"installTypeLabel": "Installation type",
|
||||||
"rootTypeLabel": "Root",
|
"rootTypeLabel": "Root",
|
||||||
"nonRootTypeLabel": "Non-root",
|
"nonRootTypeLabel": "Non-root",
|
||||||
|
|||||||
11
crowdin.yml
11
crowdin.yml
@@ -1,9 +1,4 @@
|
|||||||
project_id_env: CROWDIN_PROJECT_ID
|
preserve_hierarchy: 1
|
||||||
api_token_env: CROWDIN_PERSONAL_TOKEN
|
|
||||||
|
|
||||||
commit_message: 'chore(i18n): sync translations'
|
|
||||||
|
|
||||||
preserve_hierarchy: true
|
|
||||||
files:
|
files:
|
||||||
- source: assets/i18n/en_US.json
|
- source: /assets/i18n/en_US.json
|
||||||
translation: assets/i18n/%locale_with_underscore%.json
|
translation: /assets/i18n/%locale_with_underscore%.json
|
||||||
|
|||||||
@@ -7,16 +7,27 @@ The following pages will guide you through using ReVanced Manager to patch apps.
|
|||||||
1. Navigate to the **Patcher** tab from the bottom navigation bar
|
1. Navigate to the **Patcher** tab from the bottom navigation bar
|
||||||
2. Tap on the **Select an app** card
|
2. Tap on the **Select an app** card
|
||||||
3. Choose an app to patch[^1]
|
3. Choose an app to patch[^1]
|
||||||
> **Note**: The suggested version is visible in each app's card.
|
|
||||||
4. Tap on the **Select patches** card and select the patches you want to apply[^2]
|
|
||||||
> **Warning**: If you see a warning you can click on it for more information.
|
|
||||||
5. Tap on the **Done** then **Patch** button
|
|
||||||
> **Warning**: The patching process may take ~5 minutes. Exiting the app may increase the time it takes to patch.
|
|
||||||
6. Tap on the **Install** button
|
|
||||||
> **Note**: If you are rooted, you can mount the patched app on top of the original app.[^3]
|
|
||||||
> Optionally, you may export the patched app to storage using the options in the top right corner.
|
|
||||||
|
|
||||||
[^1]: Non-root users may be prompted to select an APK from storage, in which case you have to source the APK file yourself. ReVanced does not provide any APK files.
|
> [!NOTE]
|
||||||
|
> The suggested version is visible in each app's card.
|
||||||
|
4. Tap on the **Select patches** card and select the patches you want to apply[^2].
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> Some patches have options that can or must be configured by tapping on ⚙️ icon next to the patch name.
|
||||||
|
|
||||||
|
>[!WARNING]
|
||||||
|
> If you see a warning you can click on it for more information.
|
||||||
|
5. Tap on the **Done** then **Patch** button
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> The patching process may take ~5 minutes. Exiting the app may cancel patching or increase the time it takes to patch.
|
||||||
|
6. Tap on the **Install** button
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> If you are rooted, you can mount the patched app on top of the original app.[^3]
|
||||||
|
> Optionally, you may export the patched app to storage using the option in the bottom left corner.
|
||||||
|
|
||||||
|
[^1]: Non-root users may be prompted to select an APK from storage, in which case you must source the APK file yourself. ReVanced does not provide any APK files.
|
||||||
[^2]: It is suggested to use the default set of patches by tapping on the **Default** button above the list of patches.
|
[^2]: It is suggested to use the default set of patches by tapping on the **Default** button above the list of patches.
|
||||||
[^3]: Mounting the patched app on top of the original app will only work if the installed app version matches the version of the app selected in step 3. above.
|
[^3]: Mounting the patched app on top of the original app will only work if the installed app version matches the version of the app selected in step 3. above.
|
||||||
|
|
||||||
|
|||||||
@@ -2,33 +2,46 @@
|
|||||||
|
|
||||||
ReVanced Manager has settings that can be configured to your liking.
|
ReVanced Manager has settings that can be configured to your liking.
|
||||||
|
|
||||||
## ⭐ Essential settings
|
## 🎛️ Essential settings
|
||||||
|
|
||||||
- ### 🔗 API URL
|
- ### 🪛 Allow changing patch selection
|
||||||
|
|
||||||
Specify the URL of the API to use. This is used to fetch ReVanced Patches and update ReVanced Manager.
|
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
|
- ### 🧬 Sources
|
||||||
|
|
||||||
Override the API and change the source of ReVanced Patches.
|
Override the API and download patches from a different source.
|
||||||
|
|
||||||
- ### 🧪 Experimental ReVanced Patches support
|
- ### 🔗 API URL
|
||||||
|
|
||||||
Lift app version constraints from ReVanced Patches. This allows you to patch any version of an app, even if the patch is not explicitly compatible with it.
|
API to use to fetch updates and patches from.
|
||||||
|
|
||||||
- ### 🧑🔬 Experimental universal support
|
- ### 💾 Imports & Exports
|
||||||
|
|
||||||
This will show or hide ReVanced Patches, which are not meant for any app in particular but rather for all apps but may not work on all apps.
|
You can import, export or reset the following settings:
|
||||||
|
|
||||||
- ### 🔑 Export, import or delete keystore
|
- 🔑 Keystore
|
||||||
|
- 📄 Patch selection
|
||||||
|
- ⚙️ Patch options
|
||||||
|
|
||||||
Manage the keystore used to sign patched apps.
|
> [!NOTE]
|
||||||
|
> This is particularly useful if you want to backup or reset your settings.
|
||||||
|
|
||||||
- ### 📄 Export, import or reset ReVanced Patches selection
|
- ### ❓ About
|
||||||
|
|
||||||
Manage the ReVanced Patches selection. This is useful if you want to share your ReVanced Patches selection with others or reset it to the default selection.
|
|
||||||
|
|
||||||
- ### ℹ️ About
|
|
||||||
|
|
||||||
View information about your device and ReVanced Manager. This includes the version of ReVanced Manager and supported architectures of your device.
|
View information about your device and ReVanced Manager. This includes the version of ReVanced Manager and supported architectures of your device.
|
||||||
|
|
||||||
@@ -36,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.
|
The next page will bring you back to the usage page.
|
||||||
|
|
||||||
Continue: [🛠️ Usage](2_usage.md)
|
Continue: [🛠️ Usage](2_usage.md)
|
||||||
@@ -6,7 +6,7 @@ In case you encounter any issues while using ReVanced Manager, please refer to t
|
|||||||
|
|
||||||
Make sure ReVanced Manager is up to date by following [🔄 Updating ReVanced Manager](2_3_updating.md) and select the **Default** button when choosing patches.
|
Make sure ReVanced Manager is up to date by following [🔄 Updating ReVanced Manager](2_3_updating.md) and select the **Default** button when choosing patches.
|
||||||
|
|
||||||
- 💥 App not installed as package conflicts with an existing package
|
- 🚫 App not installed as package conflicts with an existing package
|
||||||
|
|
||||||
An existing installation of the app you're trying to patch is conflicting with the patched app. Uninstall the existing app before installing the patched app.
|
An existing installation of the app you're trying to patch is conflicting with the patched app. Uninstall the existing app before installing the patched app.
|
||||||
|
|
||||||
@@ -16,10 +16,6 @@ In case you encounter any issues while using ReVanced Manager, please refer to t
|
|||||||
|
|
||||||
Alternatively, you can use [ReVanced CLI](https://github.com/revanced/revanced-cli) to patch the app.
|
Alternatively, you can use [ReVanced CLI](https://github.com/revanced/revanced-cli) to patch the app.
|
||||||
|
|
||||||
- 🚫 Non-root install is not possible with the current patches selection
|
|
||||||
|
|
||||||
Select the **Default** button when choosing patches.
|
|
||||||
|
|
||||||
- 🚨 Patched app crashes on launch
|
- 🚨 Patched app crashes on launch
|
||||||
|
|
||||||
Select the **Default** button when choosing patches.
|
Select the **Default** button when choosing patches.
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ This page will guide you through building ReVanced Manager from source.
|
|||||||
|
|
||||||
3. Create a GitHub personal access token with the `read:packages` scope [here](https://github.com/settings/tokens/new?scopes=read:packages&description=ReVanced)
|
3. Create a GitHub personal access token with the `read:packages` scope [here](https://github.com/settings/tokens/new?scopes=read:packages&description=ReVanced)
|
||||||
|
|
||||||
4. Add your GitHub username and the token to `~/.gradle/gradle.properties`
|
4. Add your GitHub username and the token to `~/android/gradle.properties`
|
||||||
|
|
||||||
```properties
|
```properties
|
||||||
gpr.user = YourUsername
|
gpr.user = YourUsername
|
||||||
@@ -31,7 +31,8 @@ This page will guide you through building ReVanced Manager from source.
|
|||||||
flutter packages pub run build_runner build --delete-conflicting-outputs
|
flutter packages pub 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
|
7. Build the APK
|
||||||
|
|
||||||
|
|||||||
BIN
fonts/custom-icons.ttf
Normal file
BIN
fonts/custom-icons.ttf
Normal file
Binary file not shown.
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
5
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
5
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
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
Normal file
240
gradlew
vendored
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
#!/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
Normal file
91
gradlew.bat
vendored
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
@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
|
||||||
@@ -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/github_api.dart';
|
||||||
import 'package:revanced_manager/services/manager_api.dart';
|
import 'package:revanced_manager/services/manager_api.dart';
|
||||||
import 'package:revanced_manager/services/patcher_api.dart';
|
import 'package:revanced_manager/services/patcher_api.dart';
|
||||||
@@ -9,6 +10,8 @@ import 'package:revanced_manager/ui/views/home/home_viewmodel.dart';
|
|||||||
import 'package:revanced_manager/ui/views/installer/installer_view.dart';
|
import 'package:revanced_manager/ui/views/installer/installer_view.dart';
|
||||||
import 'package:revanced_manager/ui/views/navigation/navigation_view.dart';
|
import 'package:revanced_manager/ui/views/navigation/navigation_view.dart';
|
||||||
import 'package:revanced_manager/ui/views/navigation/navigation_viewmodel.dart';
|
import 'package:revanced_manager/ui/views/navigation/navigation_viewmodel.dart';
|
||||||
|
import 'package:revanced_manager/ui/views/patch_options/patch_options_view.dart';
|
||||||
|
import 'package:revanced_manager/ui/views/patch_options/patch_options_viewmodel.dart';
|
||||||
import 'package:revanced_manager/ui/views/patcher/patcher_view.dart';
|
import 'package:revanced_manager/ui/views/patcher/patcher_view.dart';
|
||||||
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
|
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
|
||||||
import 'package:revanced_manager/ui/views/patches_selector/patches_selector_view.dart';
|
import 'package:revanced_manager/ui/views/patches_selector/patches_selector_view.dart';
|
||||||
@@ -23,6 +26,7 @@ import 'package:stacked_services/stacked_services.dart';
|
|||||||
MaterialRoute(page: PatcherView),
|
MaterialRoute(page: PatcherView),
|
||||||
MaterialRoute(page: AppSelectorView),
|
MaterialRoute(page: AppSelectorView),
|
||||||
MaterialRoute(page: PatchesSelectorView),
|
MaterialRoute(page: PatchesSelectorView),
|
||||||
|
MaterialRoute(page: PatchOptionsView),
|
||||||
MaterialRoute(page: InstallerView),
|
MaterialRoute(page: InstallerView),
|
||||||
MaterialRoute(page: SettingsView),
|
MaterialRoute(page: SettingsView),
|
||||||
MaterialRoute(page: ContributorsView),
|
MaterialRoute(page: ContributorsView),
|
||||||
@@ -32,11 +36,13 @@ import 'package:stacked_services/stacked_services.dart';
|
|||||||
LazySingleton(classType: NavigationViewModel),
|
LazySingleton(classType: NavigationViewModel),
|
||||||
LazySingleton(classType: HomeViewModel),
|
LazySingleton(classType: HomeViewModel),
|
||||||
LazySingleton(classType: PatcherViewModel),
|
LazySingleton(classType: PatcherViewModel),
|
||||||
|
LazySingleton(classType: PatchOptionsViewModel),
|
||||||
LazySingleton(classType: NavigationService),
|
LazySingleton(classType: NavigationService),
|
||||||
LazySingleton(classType: ManagerAPI),
|
LazySingleton(classType: ManagerAPI),
|
||||||
LazySingleton(classType: PatcherAPI),
|
LazySingleton(classType: PatcherAPI),
|
||||||
LazySingleton(classType: RevancedAPI),
|
LazySingleton(classType: RevancedAPI),
|
||||||
LazySingleton(classType: GithubAPI),
|
LazySingleton(classType: GithubAPI),
|
||||||
|
LazySingleton(classType: DownloadManager),
|
||||||
LazySingleton(classType: Toast),
|
LazySingleton(classType: Toast),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_i18n/flutter_i18n.dart';
|
import 'package:flutter_i18n/flutter_i18n.dart';
|
||||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
import 'package:revanced_manager/app/app.locator.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/github_api.dart';
|
||||||
import 'package:revanced_manager/services/manager_api.dart';
|
import 'package:revanced_manager/services/manager_api.dart';
|
||||||
import 'package:revanced_manager/services/revanced_api.dart';
|
import 'package:revanced_manager/services/revanced_api.dart';
|
||||||
@@ -19,6 +20,7 @@ Future main() async {
|
|||||||
await setupLocator();
|
await setupLocator();
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
await locator<ManagerAPI>().initialize();
|
await locator<ManagerAPI>().initialize();
|
||||||
|
await locator<DownloadManager>().initialize();
|
||||||
final String apiUrl = locator<ManagerAPI>().getApiUrl();
|
final String apiUrl = locator<ManagerAPI>().getApiUrl();
|
||||||
await locator<RevancedAPI>().initialize(apiUrl);
|
await locator<RevancedAPI>().initialize(apiUrl);
|
||||||
final String repoUrl = locator<ManagerAPI>().getRepoUrl();
|
final String repoUrl = locator<ManagerAPI>().getRepoUrl();
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import 'package:json_annotation/json_annotation.dart';
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
import 'package:revanced_manager/utils/string.dart';
|
|
||||||
|
|
||||||
part 'patch.g.dart';
|
part 'patch.g.dart';
|
||||||
|
|
||||||
@@ -9,26 +8,29 @@ class Patch {
|
|||||||
required this.name,
|
required this.name,
|
||||||
required this.description,
|
required this.description,
|
||||||
required this.excluded,
|
required this.excluded,
|
||||||
required this.dependencies,
|
|
||||||
required this.compatiblePackages,
|
required this.compatiblePackages,
|
||||||
|
required this.options,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory Patch.fromJson(Map<String, dynamic> json) => _$PatchFromJson(json);
|
factory Patch.fromJson(Map<String, dynamic> json) {
|
||||||
|
// See: https://github.com/ReVanced/revanced-manager/issues/1364#issuecomment-1760414618
|
||||||
|
if (json['options'] == null) {
|
||||||
|
json['options'] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return _$PatchFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
final String name;
|
final String name;
|
||||||
final String description;
|
final String? description;
|
||||||
final bool excluded;
|
final bool excluded;
|
||||||
final List<String> dependencies;
|
|
||||||
final List<Package> compatiblePackages;
|
final List<Package> compatiblePackages;
|
||||||
|
final List<Option> options;
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => _$PatchToJson(this);
|
Map<String, dynamic> toJson() => _$PatchToJson(this);
|
||||||
|
|
||||||
String getSimpleName() {
|
String getSimpleName() {
|
||||||
return name
|
return name;
|
||||||
.replaceAll('-', ' ')
|
|
||||||
.split('-')
|
|
||||||
.join(' ')
|
|
||||||
.toTitleCase()
|
|
||||||
.replaceFirst('Microg', 'MicroG');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,8 +43,32 @@ class Package {
|
|||||||
|
|
||||||
factory Package.fromJson(Map<String, dynamic> json) =>
|
factory Package.fromJson(Map<String, dynamic> json) =>
|
||||||
_$PackageFromJson(json);
|
_$PackageFromJson(json);
|
||||||
|
|
||||||
final String name;
|
final String name;
|
||||||
final List<String> versions;
|
final List<String> versions;
|
||||||
|
|
||||||
Map toJson() => _$PackageToJson(this);
|
Map toJson() => _$PackageToJson(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class Option {
|
||||||
|
Option({
|
||||||
|
required this.key,
|
||||||
|
required this.title,
|
||||||
|
required this.description,
|
||||||
|
required this.value,
|
||||||
|
required this.required,
|
||||||
|
required this.optionClassType,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory Option.fromJson(Map<String, dynamic> json) => _$OptionFromJson(json);
|
||||||
|
|
||||||
|
final String key;
|
||||||
|
final String title;
|
||||||
|
final String description;
|
||||||
|
dynamic value;
|
||||||
|
final bool required;
|
||||||
|
final String optionClassType;
|
||||||
|
|
||||||
|
Map toJson() => _$OptionToJson(this);
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,23 +9,19 @@ class PatchedApplication {
|
|||||||
PatchedApplication({
|
PatchedApplication({
|
||||||
required this.name,
|
required this.name,
|
||||||
required this.packageName,
|
required this.packageName,
|
||||||
required this.originalPackageName,
|
|
||||||
required this.version,
|
required this.version,
|
||||||
required this.apkFilePath,
|
required this.apkFilePath,
|
||||||
required this.icon,
|
required this.icon,
|
||||||
required this.patchDate,
|
required this.patchDate,
|
||||||
this.isRooted = false,
|
this.isRooted = false,
|
||||||
this.isFromStorage = false,
|
this.isFromStorage = false,
|
||||||
this.hasUpdates = false,
|
|
||||||
this.appliedPatches = const [],
|
this.appliedPatches = const [],
|
||||||
this.changelog = const [],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
factory PatchedApplication.fromJson(Map<String, dynamic> json) =>
|
factory PatchedApplication.fromJson(Map<String, dynamic> json) =>
|
||||||
_$PatchedApplicationFromJson(json);
|
_$PatchedApplicationFromJson(json);
|
||||||
String name;
|
String name;
|
||||||
String packageName;
|
String packageName;
|
||||||
String originalPackageName;
|
|
||||||
String version;
|
String version;
|
||||||
final String apkFilePath;
|
final String apkFilePath;
|
||||||
@JsonKey(
|
@JsonKey(
|
||||||
@@ -36,9 +32,7 @@ class PatchedApplication {
|
|||||||
DateTime patchDate;
|
DateTime patchDate;
|
||||||
bool isRooted;
|
bool isRooted;
|
||||||
bool isFromStorage;
|
bool isFromStorage;
|
||||||
bool hasUpdates;
|
|
||||||
List<String> appliedPatches;
|
List<String> appliedPatches;
|
||||||
List<String> changelog;
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => _$PatchedApplicationToJson(this);
|
Map<String, dynamic> toJson() => _$PatchedApplicationToJson(this);
|
||||||
|
|
||||||
|
|||||||
75
lib/services/download_manager.dart
Normal file
75
lib/services/download_manager.dart
Normal 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,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,61 +1,24 @@
|
|||||||
import 'dart:convert';
|
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:dio_cache_interceptor/dio_cache_interceptor.dart';
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
|
||||||
import 'package:injectable/injectable.dart';
|
import 'package:injectable/injectable.dart';
|
||||||
import 'package:revanced_manager/app/app.locator.dart';
|
import 'package:revanced_manager/app/app.locator.dart';
|
||||||
import 'package:revanced_manager/models/patch.dart';
|
import 'package:revanced_manager/services/download_manager.dart';
|
||||||
import 'package:revanced_manager/services/manager_api.dart';
|
import 'package:revanced_manager/services/manager_api.dart';
|
||||||
|
|
||||||
@lazySingleton
|
@lazySingleton
|
||||||
class GithubAPI {
|
class GithubAPI {
|
||||||
late Dio _dio = Dio();
|
late final Dio _dio;
|
||||||
late final ManagerAPI _managerAPI = locator<ManagerAPI>();
|
late final ManagerAPI _managerAPI = locator<ManagerAPI>();
|
||||||
|
late final DownloadManager _downloadManager = locator<DownloadManager>();
|
||||||
final _cacheOptions = CacheOptions(
|
|
||||||
store: MemCacheStore(),
|
|
||||||
maxStale: const Duration(days: 1),
|
|
||||||
priority: CachePriority.high,
|
|
||||||
);
|
|
||||||
|
|
||||||
final Map<String, String> repoAppPath = {
|
|
||||||
'com.google.android.youtube': 'youtube',
|
|
||||||
'com.google.android.apps.youtube.music': 'music',
|
|
||||||
'com.twitter.android': 'twitter',
|
|
||||||
'com.reddit.frontpage': 'reddit',
|
|
||||||
'com.zhiliaoapp.musically': 'tiktok',
|
|
||||||
'de.dwd.warnapp': 'warnwetter',
|
|
||||||
'com.garzotto.pflotsh.ecmwf_a': 'ecmwf',
|
|
||||||
'com.spotify.music': 'spotify',
|
|
||||||
};
|
|
||||||
|
|
||||||
Future<void> initialize(String repoUrl) async {
|
Future<void> initialize(String repoUrl) async {
|
||||||
try {
|
_dio = _downloadManager.initDio(repoUrl);
|
||||||
_dio = Dio(
|
|
||||||
BaseOptions(
|
|
||||||
baseUrl: repoUrl,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
_dio.interceptors.add(DioCacheInterceptor(options: _cacheOptions));
|
|
||||||
} on Exception catch (e) {
|
|
||||||
if (kDebugMode) {
|
|
||||||
print(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> clearAllCache() async {
|
Future<void> clearAllCache() async {
|
||||||
try {
|
await _downloadManager.clearAllCache();
|
||||||
await _cacheOptions.store!.clean();
|
|
||||||
} on Exception catch (e) {
|
|
||||||
if (kDebugMode) {
|
|
||||||
print(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Map<String, dynamic>?> getLatestRelease(
|
Future<Map<String, dynamic>?> getLatestRelease(
|
||||||
@@ -117,7 +80,7 @@ class GithubAPI {
|
|||||||
final Map<String, dynamic> releases = response.data[0];
|
final Map<String, dynamic> releases = response.data[0];
|
||||||
int updates = 0;
|
int updates = 0;
|
||||||
final String currentVersion =
|
final String currentVersion =
|
||||||
await ManagerAPI().getCurrentManagerVersion();
|
await _managerAPI.getCurrentManagerVersion();
|
||||||
while (response.data[updates]['tag_name'] != 'v$currentVersion') {
|
while (response.data[updates]['tag_name'] != 'v$currentVersion') {
|
||||||
updates++;
|
updates++;
|
||||||
}
|
}
|
||||||
@@ -142,38 +105,6 @@ class GithubAPI {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<String>> getCommits(
|
|
||||||
String packageName,
|
|
||||||
String repoName,
|
|
||||||
DateTime since,
|
|
||||||
) async {
|
|
||||||
final String path =
|
|
||||||
'src/main/kotlin/app/revanced/patches/${repoAppPath[packageName]}';
|
|
||||||
try {
|
|
||||||
final response = await _dio.get(
|
|
||||||
'/repos/$repoName/commits',
|
|
||||||
queryParameters: {
|
|
||||||
'path': path,
|
|
||||||
'since': since.toIso8601String(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
final List<dynamic> commits = response.data;
|
|
||||||
return commits
|
|
||||||
.map(
|
|
||||||
(commit) => commit['commit']['message'].split('\n')[0] +
|
|
||||||
' - ' +
|
|
||||||
commit['commit']['author']['name'] +
|
|
||||||
'\n' as String,
|
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
} on Exception catch (e) {
|
|
||||||
if (kDebugMode) {
|
|
||||||
print(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<File?> getLatestReleaseFile(
|
Future<File?> getLatestReleaseFile(
|
||||||
String extension,
|
String extension,
|
||||||
String repoName,
|
String repoName,
|
||||||
@@ -186,7 +117,7 @@ class GithubAPI {
|
|||||||
(asset) => (asset['name'] as String).endsWith(extension),
|
(asset) => (asset['name'] as String).endsWith(extension),
|
||||||
);
|
);
|
||||||
if (asset != null) {
|
if (asset != null) {
|
||||||
return await DefaultCacheManager().getSingleFile(
|
return await _downloadManager.getSingleFile(
|
||||||
asset['browser_download_url'],
|
asset['browser_download_url'],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -207,7 +138,7 @@ class GithubAPI {
|
|||||||
) async {
|
) async {
|
||||||
try {
|
try {
|
||||||
if (url.isNotEmpty) {
|
if (url.isNotEmpty) {
|
||||||
return await DefaultCacheManager().getSingleFile(
|
return await _downloadManager.getSingleFile(
|
||||||
url,
|
url,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -222,12 +153,10 @@ class GithubAPI {
|
|||||||
final String downloadUrl = asset['browser_download_url'];
|
final String downloadUrl = asset['browser_download_url'];
|
||||||
if (extension == '.apk') {
|
if (extension == '.apk') {
|
||||||
_managerAPI.setIntegrationsDownloadURL(downloadUrl);
|
_managerAPI.setIntegrationsDownloadURL(downloadUrl);
|
||||||
} else if (extension == '.json') {
|
|
||||||
_managerAPI.setPatchesDownloadURL(downloadUrl, false);
|
|
||||||
} else {
|
} else {
|
||||||
_managerAPI.setPatchesDownloadURL(downloadUrl, true);
|
_managerAPI.setPatchesDownloadURL(downloadUrl);
|
||||||
}
|
}
|
||||||
return await DefaultCacheManager().getSingleFile(
|
return await _downloadManager.getSingleFile(
|
||||||
downloadUrl,
|
downloadUrl,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -239,30 +168,4 @@ class GithubAPI {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<Patch>> getPatches(
|
|
||||||
String repoName,
|
|
||||||
String version,
|
|
||||||
String url,
|
|
||||||
) async {
|
|
||||||
List<Patch> patches = [];
|
|
||||||
try {
|
|
||||||
final File? f = await getPatchesReleaseFile(
|
|
||||||
'.json',
|
|
||||||
repoName,
|
|
||||||
version,
|
|
||||||
url,
|
|
||||||
);
|
|
||||||
if (f != null) {
|
|
||||||
final List<dynamic> list = jsonDecode(f.readAsStringSync());
|
|
||||||
patches = list.map((patch) => Patch.fromJson(patch)).toList();
|
|
||||||
}
|
|
||||||
} on Exception catch (e) {
|
|
||||||
if (kDebugMode) {
|
|
||||||
print(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return patches;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import 'package:revanced_manager/app/app.locator.dart';
|
|||||||
import 'package:revanced_manager/models/patch.dart';
|
import 'package:revanced_manager/models/patch.dart';
|
||||||
import 'package:revanced_manager/models/patched_application.dart';
|
import 'package:revanced_manager/models/patched_application.dart';
|
||||||
import 'package:revanced_manager/services/github_api.dart';
|
import 'package:revanced_manager/services/github_api.dart';
|
||||||
|
import 'package:revanced_manager/services/patcher_api.dart';
|
||||||
import 'package:revanced_manager/services/revanced_api.dart';
|
import 'package:revanced_manager/services/revanced_api.dart';
|
||||||
import 'package:revanced_manager/services/root_api.dart';
|
import 'package:revanced_manager/services/root_api.dart';
|
||||||
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
|
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
|
||||||
@@ -26,6 +27,11 @@ class ManagerAPI {
|
|||||||
final String patcherRepo = 'revanced-patcher';
|
final String patcherRepo = 'revanced-patcher';
|
||||||
final String cliRepo = 'revanced-cli';
|
final String cliRepo = 'revanced-cli';
|
||||||
late SharedPreferences _prefs;
|
late SharedPreferences _prefs;
|
||||||
|
List<Patch> patches = [];
|
||||||
|
List<Option> modifiedOptions = [];
|
||||||
|
List<Option> options = [];
|
||||||
|
Patch? selectedPatch;
|
||||||
|
BuildContext? ctx;
|
||||||
bool isRooted = false;
|
bool isRooted = false;
|
||||||
String storedPatchesFile = '/selected-patches.json';
|
String storedPatchesFile = '/selected-patches.json';
|
||||||
String keystoreFile =
|
String keystoreFile =
|
||||||
@@ -40,12 +46,14 @@ class ManagerAPI {
|
|||||||
String defaultManagerRepo = 'revanced/revanced-manager';
|
String defaultManagerRepo = 'revanced/revanced-manager';
|
||||||
String? patchesVersion = '';
|
String? patchesVersion = '';
|
||||||
String? integrationsVersion = '';
|
String? integrationsVersion = '';
|
||||||
|
|
||||||
bool isDefaultPatchesRepo() {
|
bool isDefaultPatchesRepo() {
|
||||||
return getPatchesRepo() == 'revanced/revanced-patches';
|
return getPatchesRepo().toLowerCase() == 'revanced/revanced-patches';
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isDefaultIntegrationsRepo() {
|
bool isDefaultIntegrationsRepo() {
|
||||||
return getIntegrationsRepo() == 'revanced/revanced-integrations';
|
return getIntegrationsRepo().toLowerCase() ==
|
||||||
|
'revanced/revanced-integrations';
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> initialize() async {
|
Future<void> initialize() async {
|
||||||
@@ -79,12 +87,12 @@ class ManagerAPI {
|
|||||||
await _prefs.setString('repoUrl', url);
|
await _prefs.setString('repoUrl', url);
|
||||||
}
|
}
|
||||||
|
|
||||||
String getPatchesDownloadURL(bool bundle) {
|
String getPatchesDownloadURL() {
|
||||||
return _prefs.getString('patchesDownloadURL-$bundle') ?? '';
|
return _prefs.getString('patchesDownloadURL') ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> setPatchesDownloadURL(String value, bool bundle) async {
|
Future<void> setPatchesDownloadURL(String value) async {
|
||||||
await _prefs.setString('patchesDownloadURL-$bundle', value);
|
await _prefs.setString('patchesDownloadURL', value);
|
||||||
}
|
}
|
||||||
|
|
||||||
String getPatchesRepo() {
|
String getPatchesRepo() {
|
||||||
@@ -111,17 +119,6 @@ class ManagerAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool isPatchesChangeEnabled() {
|
bool isPatchesChangeEnabled() {
|
||||||
if (getPatchedApps().isNotEmpty && !isChangingToggleModified()) {
|
|
||||||
for (final apps in getPatchedApps()) {
|
|
||||||
if (getSavedPatches(apps.originalPackageName)
|
|
||||||
.indexWhere((patch) => patch.excluded) !=
|
|
||||||
-1) {
|
|
||||||
setPatchesChangeWarning(false);
|
|
||||||
setPatchesChangeEnabled(true);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return _prefs.getBool('patchesChangeEnabled') ?? false;
|
return _prefs.getBool('patchesChangeEnabled') ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,6 +186,29 @@ class ManagerAPI {
|
|||||||
await _prefs.setStringList('usedPatches-$packageName', patchesJson);
|
await _prefs.setStringList('usedPatches-$packageName', patchesJson);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Option? getPatchOption(String packageName, String patchName, String key) {
|
||||||
|
final String? optionJson =
|
||||||
|
_prefs.getString('patchOption-$packageName-$patchName-$key');
|
||||||
|
if (optionJson != null) {
|
||||||
|
final Option option = Option.fromJson(jsonDecode(optionJson));
|
||||||
|
return option;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void setPatchOption(Option option, String patchName, String packageName) {
|
||||||
|
final String optionJson = jsonEncode(option.toJson());
|
||||||
|
_prefs.setString(
|
||||||
|
'patchOption-$packageName-$patchName-${option.key}',
|
||||||
|
optionJson,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearPatchOption(String packageName, String patchName, String key) {
|
||||||
|
_prefs.remove('patchOption-$packageName-$patchName-$key');
|
||||||
|
}
|
||||||
|
|
||||||
String getIntegrationsRepo() {
|
String getIntegrationsRepo() {
|
||||||
return _prefs.getString('integrationsRepo') ?? defaultIntegrationsRepo;
|
return _prefs.getString('integrationsRepo') ?? defaultIntegrationsRepo;
|
||||||
}
|
}
|
||||||
@@ -208,12 +228,12 @@ class ManagerAPI {
|
|||||||
await _prefs.setBool('useDynamicTheme', value);
|
await _prefs.setBool('useDynamicTheme', value);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool getUseDarkTheme() {
|
int getThemeMode() {
|
||||||
return _prefs.getBool('useDarkTheme') ?? false;
|
return _prefs.getInt('themeMode') ?? 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> setUseDarkTheme(bool value) async {
|
Future<void> setThemeMode(int value) async {
|
||||||
await _prefs.setBool('useDarkTheme', value);
|
await _prefs.setInt('themeMode', value);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool areUniversalPatchesEnabled() {
|
bool areUniversalPatchesEnabled() {
|
||||||
@@ -224,12 +244,12 @@ class ManagerAPI {
|
|||||||
await _prefs.setBool('universalPatchesEnabled', value);
|
await _prefs.setBool('universalPatchesEnabled', value);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool areExperimentalPatchesEnabled() {
|
bool isVersionCompatibilityCheckEnabled() {
|
||||||
return _prefs.getBool('experimentalPatchesEnabled') ?? false;
|
return _prefs.getBool('versionCompatibilityCheckEnabled') ?? true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> enableExperimentalPatchesStatus(bool value) async {
|
Future<void> enableVersionCompatibilityCheckStatus(bool value) async {
|
||||||
await _prefs.setBool('experimentalPatchesEnabled', value);
|
await _prefs.setBool('versionCompatibilityCheckEnabled', value);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> setKeystorePassword(String password) async {
|
Future<void> setKeystorePassword(String password) async {
|
||||||
@@ -311,28 +331,45 @@ class ManagerAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<List<Patch>> getPatches() async {
|
Future<List<Patch>> getPatches() async {
|
||||||
try {
|
if (patches.isNotEmpty) {
|
||||||
final String repoName = getPatchesRepo();
|
return patches;
|
||||||
final String currentVersion = await getCurrentPatchesVersion();
|
|
||||||
final String url = getPatchesDownloadURL(false);
|
|
||||||
return await _githubAPI.getPatches(
|
|
||||||
repoName,
|
|
||||||
currentVersion,
|
|
||||||
url,
|
|
||||||
);
|
|
||||||
} on Exception catch (e) {
|
|
||||||
if (kDebugMode) {
|
|
||||||
print(e);
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
}
|
}
|
||||||
|
final File? patchBundleFile = await downloadPatches();
|
||||||
|
final Directory appCache = await getTemporaryDirectory();
|
||||||
|
Directory('${appCache.path}/cache').createSync();
|
||||||
|
final Directory workDir =
|
||||||
|
Directory('${appCache.path}/cache').createTempSync('tmp-');
|
||||||
|
final Directory cacheDir = Directory('${workDir.path}/cache');
|
||||||
|
cacheDir.createSync();
|
||||||
|
if (patchBundleFile != null) {
|
||||||
|
try {
|
||||||
|
final String patchesJson = await PatcherAPI.patcherChannel.invokeMethod(
|
||||||
|
'getPatches',
|
||||||
|
{
|
||||||
|
'patchBundleFilePath': patchBundleFile.path,
|
||||||
|
'cacheDirPath': cacheDir.path,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
final List<dynamic> patchesJsonList = jsonDecode(patchesJson);
|
||||||
|
patches = patchesJsonList
|
||||||
|
.map((patchJson) => Patch.fromJson(patchJson))
|
||||||
|
.toList();
|
||||||
|
return patches;
|
||||||
|
} on Exception catch (e) {
|
||||||
|
if (kDebugMode) {
|
||||||
|
print(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return List.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<File?> downloadPatches() async {
|
Future<File?> downloadPatches() async {
|
||||||
try {
|
try {
|
||||||
final String repoName = getPatchesRepo();
|
final String repoName = getPatchesRepo();
|
||||||
final String currentVersion = await getCurrentPatchesVersion();
|
final String currentVersion = await getCurrentPatchesVersion();
|
||||||
final String url = getPatchesDownloadURL(true);
|
final String url = getPatchesDownloadURL();
|
||||||
return await _githubAPI.getPatchesReleaseFile(
|
return await _githubAPI.getPatchesReleaseFile(
|
||||||
'.jar',
|
'.jar',
|
||||||
repoName,
|
repoName,
|
||||||
@@ -458,8 +495,7 @@ class ManagerAPI {
|
|||||||
|
|
||||||
Future<void> setCurrentPatchesVersion(String version) async {
|
Future<void> setCurrentPatchesVersion(String version) async {
|
||||||
await _prefs.setString('patchesVersion', version);
|
await _prefs.setString('patchesVersion', version);
|
||||||
await setPatchesDownloadURL('', false);
|
await setPatchesDownloadURL('');
|
||||||
await setPatchesDownloadURL('', true);
|
|
||||||
await downloadPatches();
|
await downloadPatches();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -495,62 +531,33 @@ class ManagerAPI {
|
|||||||
return toRemove;
|
return toRemove;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<PatchedApplication>> getUnsavedApps(
|
Future<List<PatchedApplication>> getMountedApps() async {
|
||||||
List<PatchedApplication> patchedApps,
|
final List<PatchedApplication> mountedApps = [];
|
||||||
) async {
|
|
||||||
final List<PatchedApplication> unsavedApps = [];
|
|
||||||
final bool hasRootPermissions = await _rootAPI.hasRootPermissions();
|
final bool hasRootPermissions = await _rootAPI.hasRootPermissions();
|
||||||
if (hasRootPermissions) {
|
if (hasRootPermissions) {
|
||||||
final List<String> installedApps = await _rootAPI.getInstalledApps();
|
final List<String> installedApps = await _rootAPI.getInstalledApps();
|
||||||
for (final String packageName in installedApps) {
|
for (final String packageName in installedApps) {
|
||||||
if (!patchedApps.any((app) => app.packageName == packageName)) {
|
|
||||||
final ApplicationWithIcon? application = await DeviceApps.getApp(
|
|
||||||
packageName,
|
|
||||||
true,
|
|
||||||
) as ApplicationWithIcon?;
|
|
||||||
if (application != null) {
|
|
||||||
unsavedApps.add(
|
|
||||||
PatchedApplication(
|
|
||||||
name: application.appName,
|
|
||||||
packageName: application.packageName,
|
|
||||||
originalPackageName: application.packageName,
|
|
||||||
version: application.versionName!,
|
|
||||||
apkFilePath: application.apkFilePath,
|
|
||||||
icon: application.icon,
|
|
||||||
patchDate: DateTime.now(),
|
|
||||||
isRooted: true,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
final List<Application> userApps =
|
|
||||||
await DeviceApps.getInstalledApplications();
|
|
||||||
for (final Application app in userApps) {
|
|
||||||
if (app.packageName.startsWith('app.revanced') &&
|
|
||||||
!app.packageName.startsWith('app.revanced.manager.') &&
|
|
||||||
!patchedApps.any((uapp) => uapp.packageName == app.packageName)) {
|
|
||||||
final ApplicationWithIcon? application = await DeviceApps.getApp(
|
final ApplicationWithIcon? application = await DeviceApps.getApp(
|
||||||
app.packageName,
|
packageName,
|
||||||
true,
|
true,
|
||||||
) as ApplicationWithIcon?;
|
) as ApplicationWithIcon?;
|
||||||
if (application != null) {
|
if (application != null) {
|
||||||
unsavedApps.add(
|
mountedApps.add(
|
||||||
PatchedApplication(
|
PatchedApplication(
|
||||||
name: application.appName,
|
name: application.appName,
|
||||||
packageName: application.packageName,
|
packageName: application.packageName,
|
||||||
originalPackageName: application.packageName,
|
|
||||||
version: application.versionName!,
|
version: application.versionName!,
|
||||||
apkFilePath: application.apkFilePath,
|
apkFilePath: application.apkFilePath,
|
||||||
icon: application.icon,
|
icon: application.icon,
|
||||||
patchDate: DateTime.now(),
|
patchDate: DateTime.now(),
|
||||||
|
isRooted: true,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return unsavedApps;
|
|
||||||
|
return mountedApps;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> showPatchesChangeWarningDialog(BuildContext context) {
|
Future<void> showPatchesChangeWarningDialog(BuildContext context) {
|
||||||
@@ -612,34 +619,20 @@ class ManagerAPI {
|
|||||||
|
|
||||||
Future<void> reAssessSavedApps() async {
|
Future<void> reAssessSavedApps() async {
|
||||||
final List<PatchedApplication> patchedApps = getPatchedApps();
|
final List<PatchedApplication> patchedApps = getPatchedApps();
|
||||||
final List<PatchedApplication> unsavedApps =
|
|
||||||
await getUnsavedApps(patchedApps);
|
// Remove apps that are not installed anymore.
|
||||||
patchedApps.addAll(unsavedApps);
|
|
||||||
final List<PatchedApplication> toRemove =
|
final List<PatchedApplication> toRemove =
|
||||||
await getAppsToRemove(patchedApps);
|
await getAppsToRemove(patchedApps);
|
||||||
patchedApps.removeWhere((a) => toRemove.contains(a));
|
patchedApps.removeWhere((a) => toRemove.contains(a));
|
||||||
for (final PatchedApplication app in patchedApps) {
|
|
||||||
app.hasUpdates =
|
// Determine all apps that are installed by mounting.
|
||||||
await hasAppUpdates(app.originalPackageName, app.patchDate);
|
final List<PatchedApplication> mountedApps = await getMountedApps();
|
||||||
app.changelog =
|
mountedApps.removeWhere(
|
||||||
await getAppChangelog(app.originalPackageName, app.patchDate);
|
(app) => patchedApps
|
||||||
if (!app.hasUpdates) {
|
.any((patchedApp) => patchedApp.packageName == app.packageName),
|
||||||
final String? currentInstalledVersion =
|
);
|
||||||
(await DeviceApps.getApp(app.packageName))?.versionName;
|
patchedApps.addAll(mountedApps);
|
||||||
if (currentInstalledVersion != null) {
|
|
||||||
final String currentSavedVersion = app.version;
|
|
||||||
final int currentInstalledVersionInt = int.parse(
|
|
||||||
currentInstalledVersion.replaceAll(RegExp('[^0-9]'), ''),
|
|
||||||
);
|
|
||||||
final int currentSavedVersionInt = int.parse(
|
|
||||||
currentSavedVersion.replaceAll(RegExp('[^0-9]'), ''),
|
|
||||||
);
|
|
||||||
if (currentInstalledVersionInt > currentSavedVersionInt) {
|
|
||||||
app.hasUpdates = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await setPatchedApps(patchedApps);
|
await setPatchedApps(patchedApps);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -656,37 +649,6 @@ class ManagerAPI {
|
|||||||
return !existsNonRoot;
|
return !existsNonRoot;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> hasAppUpdates(
|
|
||||||
String packageName,
|
|
||||||
DateTime patchDate,
|
|
||||||
) async {
|
|
||||||
final List<String> commits = await _githubAPI.getCommits(
|
|
||||||
packageName,
|
|
||||||
getPatchesRepo(),
|
|
||||||
patchDate,
|
|
||||||
);
|
|
||||||
return commits.isNotEmpty;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<List<String>> getAppChangelog(
|
|
||||||
String packageName,
|
|
||||||
DateTime patchDate,
|
|
||||||
) async {
|
|
||||||
List<String> newCommits = await _githubAPI.getCommits(
|
|
||||||
packageName,
|
|
||||||
getPatchesRepo(),
|
|
||||||
patchDate,
|
|
||||||
);
|
|
||||||
if (newCommits.isEmpty) {
|
|
||||||
newCommits = await _githubAPI.getCommits(
|
|
||||||
packageName,
|
|
||||||
getPatchesRepo(),
|
|
||||||
patchDate,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return newCommits;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> isSplitApk(PatchedApplication patchedApp) async {
|
Future<bool> isSplitApk(PatchedApplication patchedApp) async {
|
||||||
Application? app;
|
Application? app;
|
||||||
if (patchedApp.isFromStorage) {
|
if (patchedApp.isFromStorage) {
|
||||||
@@ -715,7 +677,7 @@ class ManagerAPI {
|
|||||||
Future<List<String>> getDefaultPatches() async {
|
Future<List<String>> getDefaultPatches() async {
|
||||||
final List<Patch> patches = await getPatches();
|
final List<Patch> patches = await getPatches();
|
||||||
final List<String> defaultPatches = [];
|
final List<String> defaultPatches = [];
|
||||||
if (areExperimentalPatchesEnabled() == false) {
|
if (isVersionCompatibilityCheckEnabled() == true) {
|
||||||
defaultPatches.addAll(
|
defaultPatches.addAll(
|
||||||
patches
|
patches
|
||||||
.where(
|
.where(
|
||||||
@@ -752,8 +714,18 @@ class ManagerAPI {
|
|||||||
return jsonDecode(string);
|
return jsonDecode(string);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void resetAllOptions() {
|
||||||
|
_prefs.getKeys().where((key) => key.startsWith('patchOption-')).forEach(
|
||||||
|
(key) {
|
||||||
|
_prefs.remove(key);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> resetLastSelectedPatches() async {
|
Future<void> resetLastSelectedPatches() async {
|
||||||
final File selectedPatchesFile = File(storedPatchesFile);
|
final File selectedPatchesFile = File(storedPatchesFile);
|
||||||
selectedPatchesFile.deleteSync();
|
if (selectedPatchesFile.existsSync()) {
|
||||||
|
selectedPatchesFile.deleteSync();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,19 +18,20 @@ import 'package:share_extend/share_extend.dart';
|
|||||||
@lazySingleton
|
@lazySingleton
|
||||||
class PatcherAPI {
|
class PatcherAPI {
|
||||||
static const patcherChannel =
|
static const patcherChannel =
|
||||||
MethodChannel('app.revanced.manager.flutter/patcher');
|
MethodChannel('app.revanced.manager.flutter/patcher');
|
||||||
final ManagerAPI _managerAPI = locator<ManagerAPI>();
|
final ManagerAPI _managerAPI = locator<ManagerAPI>();
|
||||||
final RootAPI _rootAPI = RootAPI();
|
final RootAPI _rootAPI = RootAPI();
|
||||||
late Directory _dataDir;
|
late Directory _dataDir;
|
||||||
late Directory _tmpDir;
|
late Directory _tmpDir;
|
||||||
late File _keyStoreFile;
|
late File _keyStoreFile;
|
||||||
List<Patch> _patches = [];
|
List<Patch> _patches = [];
|
||||||
|
List<Patch> _universalPatches = [];
|
||||||
|
List<String> _compatiblePackages = [];
|
||||||
Map filteredPatches = <String, List<Patch>>{};
|
Map filteredPatches = <String, List<Patch>>{};
|
||||||
File? _outFile;
|
File? outFile;
|
||||||
|
|
||||||
Future<void> initialize() async {
|
Future<void> initialize() async {
|
||||||
await _loadPatches();
|
await loadPatches();
|
||||||
await _managerAPI.downloadPatches();
|
|
||||||
await _managerAPI.downloadIntegrations();
|
await _managerAPI.downloadIntegrations();
|
||||||
final Directory appCache = await getTemporaryDirectory();
|
final Directory appCache = await getTemporaryDirectory();
|
||||||
_dataDir = await getExternalStorageDirectory() ?? appCache;
|
_dataDir = await getExternalStorageDirectory() ?? appCache;
|
||||||
@@ -45,7 +46,23 @@ class PatcherAPI {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadPatches() async {
|
List<String> getCompatiblePackages() {
|
||||||
|
final List<String> compatiblePackages = [];
|
||||||
|
for (final Patch patch in _patches) {
|
||||||
|
for (final Package package in patch.compatiblePackages) {
|
||||||
|
if (!compatiblePackages.contains(package.name)) {
|
||||||
|
compatiblePackages.add(package.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return compatiblePackages;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Patch> getUniversalPatches() {
|
||||||
|
return _patches.where((patch) => patch.compatiblePackages.isEmpty).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> loadPatches() async {
|
||||||
try {
|
try {
|
||||||
if (_patches.isEmpty) {
|
if (_patches.isEmpty) {
|
||||||
_patches = await _managerAPI.getPatches();
|
_patches = await _managerAPI.getPatches();
|
||||||
@@ -56,63 +73,59 @@ class PatcherAPI {
|
|||||||
}
|
}
|
||||||
_patches = List.empty();
|
_patches = List.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_compatiblePackages = getCompatiblePackages();
|
||||||
|
_universalPatches = getUniversalPatches();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<ApplicationWithIcon>> getFilteredInstalledApps(
|
Future<List<ApplicationWithIcon>> getFilteredInstalledApps(
|
||||||
bool showUniversalPatches,
|
bool showUniversalPatches,) async {
|
||||||
) async {
|
|
||||||
final List<ApplicationWithIcon> filteredApps = [];
|
final List<ApplicationWithIcon> filteredApps = [];
|
||||||
final bool allAppsIncluded =
|
final bool allAppsIncluded =
|
||||||
_patches.any((patch) => patch.compatiblePackages.isEmpty) &&
|
_universalPatches.isNotEmpty && showUniversalPatches;
|
||||||
showUniversalPatches;
|
|
||||||
if (allAppsIncluded) {
|
if (allAppsIncluded) {
|
||||||
final allPackages = await DeviceApps.getInstalledApplications(
|
final appList = await DeviceApps.getInstalledApplications(
|
||||||
includeAppIcons: true,
|
includeAppIcons: true,
|
||||||
onlyAppsWithLaunchIntent: true,
|
onlyAppsWithLaunchIntent: true,
|
||||||
);
|
);
|
||||||
for (final pkg in allPackages) {
|
|
||||||
if (!filteredApps.any((app) => app.packageName == pkg.packageName)) {
|
for (final app in appList) {
|
||||||
final appInfo = await DeviceApps.getApp(
|
filteredApps.add(app as ApplicationWithIcon);
|
||||||
pkg.packageName,
|
|
||||||
true,
|
|
||||||
) as ApplicationWithIcon?;
|
|
||||||
if (appInfo != null) {
|
|
||||||
filteredApps.add(appInfo);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (final Patch patch in _patches) {
|
for (final packageName in _compatiblePackages) {
|
||||||
for (final Package package in patch.compatiblePackages) {
|
try {
|
||||||
try {
|
if (!filteredApps.any((app) => app.packageName == packageName)) {
|
||||||
if (!filteredApps.any((app) => app.packageName == package.name)) {
|
final ApplicationWithIcon? app = await DeviceApps.getApp(
|
||||||
final ApplicationWithIcon? app = await DeviceApps.getApp(
|
packageName,
|
||||||
package.name,
|
true,
|
||||||
true,
|
) as ApplicationWithIcon?;
|
||||||
) as ApplicationWithIcon?;
|
if (app != null) {
|
||||||
if (app != null) {
|
filteredApps.add(app);
|
||||||
filteredApps.add(app);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} on Exception catch (e) {
|
|
||||||
if (kDebugMode) {
|
|
||||||
print(e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} on Exception catch (e) {
|
||||||
|
if (kDebugMode) {
|
||||||
|
print(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return filteredApps;
|
return filteredApps;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Patch> getFilteredPatches(String packageName) {
|
List<Patch> getFilteredPatches(String packageName) {
|
||||||
|
if (!_compatiblePackages.contains(packageName)) {
|
||||||
|
return _universalPatches;
|
||||||
|
}
|
||||||
|
|
||||||
final List<Patch> patches = _patches
|
final List<Patch> patches = _patches
|
||||||
.where(
|
.where(
|
||||||
(patch) =>
|
(patch) =>
|
||||||
patch.compatiblePackages.isEmpty ||
|
patch.compatiblePackages.isEmpty ||
|
||||||
!patch.name.contains('settings') &&
|
!patch.name.contains('settings') &&
|
||||||
patch.compatiblePackages
|
patch.compatiblePackages
|
||||||
.any((pack) => pack.name == packageName),
|
.any((pack) => pack.name == packageName),
|
||||||
)
|
)
|
||||||
.toList();
|
.toList();
|
||||||
if (!_managerAPI.areUniversalPatchesEnabled()) {
|
if (!_managerAPI.areUniversalPatchesEnabled()) {
|
||||||
filteredPatches[packageName] = patches
|
filteredPatches[packageName] = patches
|
||||||
@@ -124,77 +137,52 @@ class PatcherAPI {
|
|||||||
return filteredPatches[packageName];
|
return filteredPatches[packageName];
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<Patch>> getAppliedPatches(
|
Future<List<Patch>> getAppliedPatches(List<String> appliedPatches,) async {
|
||||||
List<String> appliedPatches,
|
|
||||||
) async {
|
|
||||||
return _patches
|
return _patches
|
||||||
.where((patch) => appliedPatches.contains(patch.name))
|
.where((patch) => appliedPatches.contains(patch.name))
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> needsResourcePatching(
|
Future<void> runPatcher(String packageName,
|
||||||
List<Patch> selectedPatches,
|
String apkFilePath,
|
||||||
) async {
|
List<Patch> selectedPatches,) async {
|
||||||
return selectedPatches.any(
|
final File? integrationsFile = await _managerAPI.downloadIntegrations();
|
||||||
(patch) => patch.dependencies.any(
|
final Map<String, Map<String, dynamic>> options = {};
|
||||||
(dep) => dep.contains('resource-'),
|
for (final patch in selectedPatches) {
|
||||||
),
|
if (patch.options.isNotEmpty) {
|
||||||
);
|
final Map<String, dynamic> patchOptions = {};
|
||||||
}
|
for (final option in patch.options) {
|
||||||
|
final patchOption = _managerAPI.getPatchOption(packageName, patch.name, option.key);
|
||||||
Future<bool> needsSettingsPatch(List<Patch> selectedPatches) async {
|
if (patchOption != null) {
|
||||||
return selectedPatches.any(
|
patchOptions[patchOption.key] = patchOption.value;
|
||||||
(patch) => patch.dependencies.any(
|
}
|
||||||
(dep) => dep.contains('settings'),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> runPatcher(
|
|
||||||
String packageName,
|
|
||||||
String apkFilePath,
|
|
||||||
List<Patch> selectedPatches,
|
|
||||||
) async {
|
|
||||||
final bool includeSettings = await needsSettingsPatch(selectedPatches);
|
|
||||||
if (includeSettings) {
|
|
||||||
try {
|
|
||||||
final Patch? settingsPatch = _patches.firstWhereOrNull(
|
|
||||||
(patch) =>
|
|
||||||
patch.name.contains('settings') &&
|
|
||||||
patch.compatiblePackages.any((pack) => pack.name == packageName),
|
|
||||||
);
|
|
||||||
if (settingsPatch != null) {
|
|
||||||
selectedPatches.add(settingsPatch);
|
|
||||||
}
|
|
||||||
} on Exception catch (e) {
|
|
||||||
if (kDebugMode) {
|
|
||||||
print(e);
|
|
||||||
}
|
}
|
||||||
|
options[patch.name] = patchOptions;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
final File? patchBundleFile = await _managerAPI.downloadPatches();
|
|
||||||
final File? integrationsFile = await _managerAPI.downloadIntegrations();
|
if (integrationsFile != null) {
|
||||||
if (patchBundleFile != null) {
|
|
||||||
_dataDir.createSync();
|
_dataDir.createSync();
|
||||||
_tmpDir.createSync();
|
_tmpDir.createSync();
|
||||||
final Directory workDir = _tmpDir.createTempSync('tmp-');
|
final Directory workDir = _tmpDir.createTempSync('tmp-');
|
||||||
final File inputFile = File('${workDir.path}/base.apk');
|
final File inputFile = File('${workDir.path}/base.apk');
|
||||||
final File patchedFile = File('${workDir.path}/patched.apk');
|
final File patchedFile = File('${workDir.path}/patched.apk');
|
||||||
_outFile = File('${workDir.path}/out.apk');
|
outFile = File('${workDir.path}/out.apk');
|
||||||
final Directory cacheDir = Directory('${workDir.path}/cache');
|
final Directory cacheDir = Directory('${workDir.path}/cache');
|
||||||
cacheDir.createSync();
|
cacheDir.createSync();
|
||||||
final String originalFilePath = apkFilePath;
|
final String originalFilePath = apkFilePath;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await patcherChannel.invokeMethod(
|
await patcherChannel.invokeMethod(
|
||||||
'runPatcher',
|
'runPatcher',
|
||||||
{
|
{
|
||||||
'patchBundleFilePath': patchBundleFile.path,
|
|
||||||
'originalFilePath': originalFilePath,
|
'originalFilePath': originalFilePath,
|
||||||
'inputFilePath': inputFile.path,
|
'inputFilePath': inputFile.path,
|
||||||
'patchedFilePath': patchedFile.path,
|
'patchedFilePath': patchedFile.path,
|
||||||
'outFilePath': _outFile!.path,
|
'outFilePath': outFile!.path,
|
||||||
'integrationsPath': integrationsFile!.path,
|
'integrationsPath': integrationsFile.path,
|
||||||
'selectedPatches': selectedPatches.map((p) => p.name).toList(),
|
'selectedPatches': selectedPatches.map((p) => p.name).toList(),
|
||||||
|
'options': options,
|
||||||
'cacheDirPath': cacheDir.path,
|
'cacheDirPath': cacheDir.path,
|
||||||
'keyStoreFilePath': _keyStoreFile.path,
|
'keyStoreFilePath': _keyStoreFile.path,
|
||||||
'keystorePassword': _managerAPI.getKeystorePassword(),
|
'keystorePassword': _managerAPI.getKeystorePassword(),
|
||||||
@@ -206,131 +194,131 @@ class PatcherAPI {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> stopPatcher() async {
|
Future<void> stopPatcher() async {
|
||||||
try {
|
try {
|
||||||
await patcherChannel.invokeMethod('stopPatcher');
|
await patcherChannel.invokeMethod('stopPatcher');
|
||||||
} on Exception catch (e) {
|
} on Exception catch (e) {
|
||||||
if (kDebugMode) {
|
if (kDebugMode) {
|
||||||
print(e);
|
print(e);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> installPatchedFile(PatchedApplication patchedApp) async {
|
|
||||||
if (_outFile != null) {
|
|
||||||
try {
|
|
||||||
if (patchedApp.isRooted) {
|
|
||||||
final bool hasRootPermissions = await _rootAPI.hasRootPermissions();
|
|
||||||
if (hasRootPermissions) {
|
|
||||||
return _rootAPI.installApp(
|
|
||||||
patchedApp.packageName,
|
|
||||||
patchedApp.apkFilePath,
|
|
||||||
_outFile!.path,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
final install = await InstallPlugin.installApk(_outFile!.path);
|
|
||||||
return install['isSuccess'];
|
|
||||||
}
|
|
||||||
} on Exception catch (e) {
|
|
||||||
if (kDebugMode) {
|
|
||||||
print(e);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
void exportPatchedFile(String appName, String version) {
|
|
||||||
try {
|
|
||||||
if (_outFile != null) {
|
|
||||||
final String newName = _getFileName(appName, version);
|
|
||||||
CRFileSaver.saveFileWithDialog(
|
|
||||||
SaveFileDialogParams(
|
|
||||||
sourceFilePath: _outFile!.path,
|
|
||||||
destinationFileName: newName,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} on Exception catch (e) {
|
|
||||||
if (kDebugMode) {
|
|
||||||
print(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void sharePatchedFile(String appName, String version) {
|
|
||||||
try {
|
|
||||||
if (_outFile != null) {
|
|
||||||
final String newName = _getFileName(appName, version);
|
|
||||||
final int lastSeparator = _outFile!.path.lastIndexOf('/');
|
|
||||||
final String newPath =
|
|
||||||
_outFile!.path.substring(0, lastSeparator + 1) + newName;
|
|
||||||
final File shareFile = _outFile!.copySync(newPath);
|
|
||||||
ShareExtend.share(shareFile.path, 'file');
|
|
||||||
}
|
|
||||||
} on Exception catch (e) {
|
|
||||||
if (kDebugMode) {
|
|
||||||
print(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String _getFileName(String appName, String version) {
|
|
||||||
final String prefix = appName.toLowerCase().replaceAll(' ', '-');
|
|
||||||
final String newName = '$prefix-revanced_v$version.apk';
|
|
||||||
return newName;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> exportPatcherLog(String logs) async {
|
|
||||||
final Directory appCache = await getTemporaryDirectory();
|
|
||||||
final Directory logDir = Directory('${appCache.path}/logs');
|
|
||||||
logDir.createSync();
|
|
||||||
final String dateTime = DateTime.now()
|
|
||||||
.toIso8601String()
|
|
||||||
.replaceAll('-', '')
|
|
||||||
.replaceAll(':', '')
|
|
||||||
.replaceAll('T', '')
|
|
||||||
.replaceAll('.', '');
|
|
||||||
final String fileName = 'revanced-manager_patcher_$dateTime.log';
|
|
||||||
final File log = File('${logDir.path}/$fileName');
|
|
||||||
log.writeAsStringSync(logs);
|
|
||||||
CRFileSaver.saveFileWithDialog(
|
|
||||||
SaveFileDialogParams(
|
|
||||||
sourceFilePath: log.path,
|
|
||||||
destinationFileName: fileName,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String getSuggestedVersion(String packageName) {
|
|
||||||
final Map<String, int> versions = {};
|
|
||||||
for (final Patch patch in _patches) {
|
|
||||||
final Package? package = patch.compatiblePackages.firstWhereOrNull(
|
|
||||||
(pack) => pack.name == packageName,
|
|
||||||
);
|
|
||||||
if (package != null) {
|
|
||||||
for (final String version in package.versions) {
|
|
||||||
versions.update(
|
|
||||||
version,
|
|
||||||
(value) => versions[version]! + 1,
|
|
||||||
ifAbsent: () => 1,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (versions.isNotEmpty) {
|
|
||||||
final entries = versions.entries.toList()
|
|
||||||
..sort((a, b) => a.value.compareTo(b.value));
|
|
||||||
versions
|
|
||||||
..clear()
|
|
||||||
..addEntries(entries);
|
|
||||||
versions.removeWhere((key, value) => value != versions.values.last);
|
|
||||||
return (versions.keys.toList()..sort()).last;
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> installPatchedFile(PatchedApplication patchedApp) async {
|
||||||
|
if (outFile != null) {
|
||||||
|
try {
|
||||||
|
if (patchedApp.isRooted) {
|
||||||
|
final bool hasRootPermissions = await _rootAPI.hasRootPermissions();
|
||||||
|
if (hasRootPermissions) {
|
||||||
|
return _rootAPI.installApp(
|
||||||
|
patchedApp.packageName,
|
||||||
|
patchedApp.apkFilePath,
|
||||||
|
outFile!.path,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
final install = await InstallPlugin.installApk(outFile!.path);
|
||||||
|
return install['isSuccess'];
|
||||||
|
}
|
||||||
|
} on Exception catch (e) {
|
||||||
|
if (kDebugMode) {
|
||||||
|
print(e);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void exportPatchedFile(String appName, String version) {
|
||||||
|
try {
|
||||||
|
if (outFile != null) {
|
||||||
|
final String newName = _getFileName(appName, version);
|
||||||
|
CRFileSaver.saveFileWithDialog(
|
||||||
|
SaveFileDialogParams(
|
||||||
|
sourceFilePath: outFile!.path,
|
||||||
|
destinationFileName: newName,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} on Exception catch (e) {
|
||||||
|
if (kDebugMode) {
|
||||||
|
print(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void sharePatchedFile(String appName, String version) {
|
||||||
|
try {
|
||||||
|
if (outFile != null) {
|
||||||
|
final String newName = _getFileName(appName, version);
|
||||||
|
final int lastSeparator = outFile!.path.lastIndexOf('/');
|
||||||
|
final String newPath =
|
||||||
|
outFile!.path.substring(0, lastSeparator + 1) + newName;
|
||||||
|
final File shareFile = outFile!.copySync(newPath);
|
||||||
|
ShareExtend.share(shareFile.path, 'file');
|
||||||
|
}
|
||||||
|
} on Exception catch (e) {
|
||||||
|
if (kDebugMode) {
|
||||||
|
print(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getFileName(String appName, String version) {
|
||||||
|
final String prefix = appName.toLowerCase().replaceAll(' ', '-');
|
||||||
|
final String newName = '$prefix-revanced_v$version.apk';
|
||||||
|
return newName;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> exportPatcherLog(String logs) async {
|
||||||
|
final Directory appCache = await getTemporaryDirectory();
|
||||||
|
final Directory logDir = Directory('${appCache.path}/logs');
|
||||||
|
logDir.createSync();
|
||||||
|
final String dateTime = DateTime.now()
|
||||||
|
.toIso8601String()
|
||||||
|
.replaceAll('-', '')
|
||||||
|
.replaceAll(':', '')
|
||||||
|
.replaceAll('T', '')
|
||||||
|
.replaceAll('.', '');
|
||||||
|
final String fileName = 'revanced-manager_patcher_$dateTime.txt';
|
||||||
|
final File log = File('${logDir.path}/$fileName');
|
||||||
|
log.writeAsStringSync(logs);
|
||||||
|
CRFileSaver.saveFileWithDialog(
|
||||||
|
SaveFileDialogParams(
|
||||||
|
sourceFilePath: log.path,
|
||||||
|
destinationFileName: fileName,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String getSuggestedVersion(String packageName) {
|
||||||
|
final Map<String, int> versions = {};
|
||||||
|
for (final Patch patch in _patches) {
|
||||||
|
final Package? package = patch.compatiblePackages.firstWhereOrNull(
|
||||||
|
(pack) => pack.name == packageName,
|
||||||
|
);
|
||||||
|
if (package != null) {
|
||||||
|
for (final String version in package.versions) {
|
||||||
|
versions.update(
|
||||||
|
version,
|
||||||
|
(value) => versions[version]! + 1,
|
||||||
|
ifAbsent: () => 1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (versions.isNotEmpty) {
|
||||||
|
final entries = versions.entries.toList()
|
||||||
|
..sort((a, b) => a.value.compareTo(b.value));
|
||||||
|
versions
|
||||||
|
..clear()
|
||||||
|
..addEntries(entries);
|
||||||
|
versions.removeWhere((key, value) => value != versions.values.last);
|
||||||
|
return (versions.keys.toList()
|
||||||
|
..sort()).last;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}}
|
||||||
|
|||||||
@@ -3,46 +3,27 @@ import 'dart:io';
|
|||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:dio_cache_interceptor/dio_cache_interceptor.dart';
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||||
import 'package:injectable/injectable.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';
|
import 'package:timeago/timeago.dart';
|
||||||
|
|
||||||
@lazySingleton
|
@lazySingleton
|
||||||
class RevancedAPI {
|
class RevancedAPI {
|
||||||
late Dio _dio = Dio();
|
late final Dio _dio;
|
||||||
|
late final DownloadManager _downloadManager = locator<DownloadManager>();
|
||||||
|
|
||||||
final _cacheOptions = CacheOptions(
|
final Lock getToolsLock = Lock();
|
||||||
store: MemCacheStore(),
|
|
||||||
maxStale: const Duration(days: 1),
|
|
||||||
priority: CachePriority.high,
|
|
||||||
);
|
|
||||||
|
|
||||||
Future<void> initialize(String apiUrl) async {
|
Future<void> initialize(String repoUrl) async {
|
||||||
try {
|
_dio = _downloadManager.initDio(repoUrl);
|
||||||
_dio = Dio(
|
|
||||||
BaseOptions(
|
|
||||||
baseUrl: apiUrl,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
_dio.interceptors.add(DioCacheInterceptor(options: _cacheOptions));
|
|
||||||
} on Exception catch (e) {
|
|
||||||
if (kDebugMode) {
|
|
||||||
print(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> clearAllCache() async {
|
Future<void> clearAllCache() async {
|
||||||
try {
|
await _downloadManager.clearAllCache();
|
||||||
await _cacheOptions.store!.clean();
|
|
||||||
} on Exception catch (e) {
|
|
||||||
if (kDebugMode) {
|
|
||||||
print(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Map<String, List<dynamic>>> getContributors() async {
|
Future<Map<String, List<dynamic>>> getContributors() async {
|
||||||
@@ -66,21 +47,23 @@ class RevancedAPI {
|
|||||||
Future<Map<String, dynamic>?> _getLatestRelease(
|
Future<Map<String, dynamic>?> _getLatestRelease(
|
||||||
String extension,
|
String extension,
|
||||||
String repoName,
|
String repoName,
|
||||||
) async {
|
) {
|
||||||
try {
|
return getToolsLock.synchronized(() async {
|
||||||
final response = await _dio.get('/tools');
|
try {
|
||||||
final List<dynamic> tools = response.data['tools'];
|
final response = await _dio.get('/tools');
|
||||||
return tools.firstWhereOrNull(
|
final List<dynamic> tools = response.data['tools'];
|
||||||
(t) =>
|
return tools.firstWhereOrNull(
|
||||||
t['repository'] == repoName &&
|
(t) =>
|
||||||
(t['name'] as String).endsWith(extension),
|
t['repository'] == repoName &&
|
||||||
);
|
(t['name'] as String).endsWith(extension),
|
||||||
} on Exception catch (e) {
|
);
|
||||||
if (kDebugMode) {
|
} on Exception catch (e) {
|
||||||
print(e);
|
if (kDebugMode) {
|
||||||
|
print(e);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
return null;
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String?> getLatestReleaseVersion(
|
Future<String?> getLatestReleaseVersion(
|
||||||
@@ -115,7 +98,7 @@ class RevancedAPI {
|
|||||||
);
|
);
|
||||||
if (release != null) {
|
if (release != null) {
|
||||||
final String url = release['browser_download_url'];
|
final String url = release['browser_download_url'];
|
||||||
return await DefaultCacheManager().getSingleFile(url);
|
return await _downloadManager.getSingleFile(url);
|
||||||
}
|
}
|
||||||
} on Exception catch (e) {
|
} on Exception catch (e) {
|
||||||
if (kDebugMode) {
|
if (kDebugMode) {
|
||||||
@@ -147,9 +130,8 @@ class RevancedAPI {
|
|||||||
'revanced/revanced-manager',
|
'revanced/revanced-manager',
|
||||||
);
|
);
|
||||||
File? outputFile;
|
File? outputFile;
|
||||||
await for (final result in DefaultCacheManager().getFileStream(
|
await for (final result in _downloadManager.getFileStream(
|
||||||
release!['browser_download_url'] as String,
|
release!['browser_download_url'] as String,
|
||||||
withProgress: true,
|
|
||||||
)) {
|
)) {
|
||||||
if (result is DownloadProgress) {
|
if (result is DownloadProgress) {
|
||||||
final totalSize = result.totalSize ?? 10000000;
|
final totalSize = result.totalSize ?? 10000000;
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
|
import 'dart:ui';
|
||||||
import 'package:dynamic_color/dynamic_color.dart';
|
import 'package:dynamic_color/dynamic_color.dart';
|
||||||
import 'package:dynamic_themes/dynamic_themes.dart';
|
import 'package:dynamic_themes/dynamic_themes.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
import 'package:revanced_manager/app/app.locator.dart';
|
||||||
import 'package:revanced_manager/app/app.router.dart';
|
import 'package:revanced_manager/app/app.router.dart';
|
||||||
|
import 'package:revanced_manager/services/manager_api.dart';
|
||||||
import 'package:revanced_manager/theme.dart';
|
import 'package:revanced_manager/theme.dart';
|
||||||
import 'package:stacked_services/stacked_services.dart';
|
import 'package:stacked_services/stacked_services.dart';
|
||||||
|
|
||||||
class DynamicThemeBuilder extends StatelessWidget {
|
class DynamicThemeBuilder extends StatefulWidget {
|
||||||
const DynamicThemeBuilder({
|
const DynamicThemeBuilder({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.title,
|
required this.title,
|
||||||
@@ -17,6 +21,35 @@ class DynamicThemeBuilder extends StatelessWidget {
|
|||||||
final Widget home;
|
final Widget home;
|
||||||
final Iterable<LocalizationsDelegate> localizationsDelegates;
|
final Iterable<LocalizationsDelegate> localizationsDelegates;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<DynamicThemeBuilder> createState() => _DynamicThemeBuilderState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DynamicThemeBuilderState extends State<DynamicThemeBuilder> with WidgetsBindingObserver {
|
||||||
|
Brightness brightness = PlatformDispatcher.instance.platformBrightness;
|
||||||
|
final ManagerAPI _managerAPI = locator<ManagerAPI>();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
WidgetsBinding.instance.addObserver(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangePlatformBrightness() {
|
||||||
|
setState(() {
|
||||||
|
brightness = PlatformDispatcher.instance.platformBrightness;
|
||||||
|
});
|
||||||
|
if (_managerAPI.getThemeMode() < 2) {
|
||||||
|
SystemChrome.setSystemUIOverlayStyle(
|
||||||
|
SystemUiOverlayStyle(
|
||||||
|
systemNavigationBarIconBrightness:
|
||||||
|
brightness == Brightness.light ? Brightness.dark : Brightness.light,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return DynamicColorBuilder(
|
return DynamicColorBuilder(
|
||||||
@@ -50,24 +83,32 @@ class DynamicThemeBuilder extends StatelessWidget {
|
|||||||
return DynamicTheme(
|
return DynamicTheme(
|
||||||
themeCollection: ThemeCollection(
|
themeCollection: ThemeCollection(
|
||||||
themes: {
|
themes: {
|
||||||
0: lightCustomTheme,
|
0: brightness == Brightness.light ? lightCustomTheme : darkCustomTheme,
|
||||||
1: darkCustomTheme,
|
1: brightness == Brightness.light ? lightDynamicTheme : darkDynamicTheme,
|
||||||
2: lightDynamicTheme,
|
2: lightCustomTheme,
|
||||||
3: darkDynamicTheme,
|
3: lightDynamicTheme,
|
||||||
|
4: darkCustomTheme,
|
||||||
|
5: darkDynamicTheme,
|
||||||
},
|
},
|
||||||
fallbackTheme: lightCustomTheme,
|
fallbackTheme: PlatformDispatcher.instance.platformBrightness == Brightness.light ? lightCustomTheme : darkCustomTheme,
|
||||||
),
|
),
|
||||||
builder: (context, theme) => MaterialApp(
|
builder: (context, theme) => MaterialApp(
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
title: title,
|
title: widget.title,
|
||||||
navigatorKey: StackedService.navigatorKey,
|
navigatorKey: StackedService.navigatorKey,
|
||||||
onGenerateRoute: StackedRouter().onGenerateRoute,
|
onGenerateRoute: StackedRouter().onGenerateRoute,
|
||||||
theme: theme,
|
theme: theme,
|
||||||
home: home,
|
home: widget.home,
|
||||||
localizationsDelegates: localizationsDelegates,
|
localizationsDelegates: widget.localizationsDelegates,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ class _AppSelectorViewState extends State<AppSelectorView> {
|
|||||||
onViewModelReady: (model) => model.initialize(),
|
onViewModelReady: (model) => model.initialize(),
|
||||||
viewModelBuilder: () => AppSelectorViewModel(),
|
viewModelBuilder: () => AppSelectorViewModel(),
|
||||||
builder: (context, model, child) => Scaffold(
|
builder: (context, model, child) => Scaffold(
|
||||||
resizeToAvoidBottomInset: false,
|
|
||||||
floatingActionButton: FloatingActionButton.extended(
|
floatingActionButton: FloatingActionButton.extended(
|
||||||
label: I18nText('appSelectorView.storageButton'),
|
label: I18nText('appSelectorView.storageButton'),
|
||||||
icon: const Icon(Icons.sd_storage),
|
icon: const Icon(Icons.sd_storage),
|
||||||
@@ -39,13 +38,11 @@ class _AppSelectorViewState extends State<AppSelectorView> {
|
|||||||
floating: true,
|
floating: true,
|
||||||
title: I18nText(
|
title: I18nText(
|
||||||
'appSelectorView.viewTitle',
|
'appSelectorView.viewTitle',
|
||||||
child: Text(
|
),
|
||||||
'',
|
titleTextStyle: TextStyle(
|
||||||
style: TextStyle(
|
fontSize: 22.0,
|
||||||
color: Theme.of(context).textTheme.titleLarge!.color,
|
color: Theme.of(context).textTheme.titleLarge!.color,
|
||||||
),
|
),
|
||||||
),
|
|
||||||
),
|
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
Icons.arrow_back,
|
Icons.arrow_back,
|
||||||
@@ -54,7 +51,7 @@ class _AppSelectorViewState extends State<AppSelectorView> {
|
|||||||
onPressed: () => Navigator.of(context).pop(),
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
),
|
),
|
||||||
bottom: PreferredSize(
|
bottom: PreferredSize(
|
||||||
preferredSize: const Size.fromHeight(64.0),
|
preferredSize: const Size.fromHeight(66.0),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
vertical: 8.0,
|
vertical: 8.0,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import 'package:revanced_manager/services/patcher_api.dart';
|
|||||||
import 'package:revanced_manager/services/toast.dart';
|
import 'package:revanced_manager/services/toast.dart';
|
||||||
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
|
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
|
||||||
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.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';
|
import 'package:stacked/stacked.dart';
|
||||||
|
|
||||||
class AppSelectorViewModel extends BaseViewModel {
|
class AppSelectorViewModel extends BaseViewModel {
|
||||||
@@ -73,13 +74,12 @@ class AppSelectorViewModel extends BaseViewModel {
|
|||||||
locator<PatcherViewModel>().selectedApp = PatchedApplication(
|
locator<PatcherViewModel>().selectedApp = PatchedApplication(
|
||||||
name: application.appName,
|
name: application.appName,
|
||||||
packageName: application.packageName,
|
packageName: application.packageName,
|
||||||
originalPackageName: application.packageName,
|
|
||||||
version: application.versionName!,
|
version: application.versionName!,
|
||||||
apkFilePath: application.apkFilePath,
|
apkFilePath: application.apkFilePath,
|
||||||
icon: application.icon,
|
icon: application.icon,
|
||||||
patchDate: DateTime.now(),
|
patchDate: DateTime.now(),
|
||||||
);
|
);
|
||||||
locator<PatcherViewModel>().loadLastSelectedPatches();
|
await locator<PatcherViewModel>().loadLastSelectedPatches();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> canSelectInstalled(
|
Future<void> canSelectInstalled(
|
||||||
@@ -90,10 +90,18 @@ class AppSelectorViewModel extends BaseViewModel {
|
|||||||
await DeviceApps.getApp(packageName, true) as ApplicationWithIcon?;
|
await DeviceApps.getApp(packageName, true) as ApplicationWithIcon?;
|
||||||
if (app != null) {
|
if (app != null) {
|
||||||
if (await checkSplitApk(packageName) && !isRooted) {
|
if (await checkSplitApk(packageName) && !isRooted) {
|
||||||
return showSelectFromStorageDialog(context);
|
if (context.mounted) {
|
||||||
|
return showSelectFromStorageDialog(context);
|
||||||
|
}
|
||||||
} else if (!await checkSplitApk(packageName) || isRooted) {
|
} else if (!await checkSplitApk(packageName) || isRooted) {
|
||||||
selectApp(app);
|
await selectApp(app);
|
||||||
Navigator.pop(context);
|
if (context.mounted) {
|
||||||
|
Navigator.pop(context);
|
||||||
|
}
|
||||||
|
final List<Option> requiredNullOptions = getNullRequiredOptions(locator<PatcherViewModel>().selectedPatches, packageName);
|
||||||
|
if(requiredNullOptions.isNotEmpty){
|
||||||
|
locator<PatcherViewModel>().showRequiredOptionDialog();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -198,7 +206,6 @@ class AppSelectorViewModel extends BaseViewModel {
|
|||||||
locator<PatcherViewModel>().selectedApp = PatchedApplication(
|
locator<PatcherViewModel>().selectedApp = PatchedApplication(
|
||||||
name: application.appName,
|
name: application.appName,
|
||||||
packageName: application.packageName,
|
packageName: application.packageName,
|
||||||
originalPackageName: application.packageName,
|
|
||||||
version: application.versionName!,
|
version: application.versionName!,
|
||||||
apkFilePath: result.files.single.path!,
|
apkFilePath: result.files.single.path!,
|
||||||
icon: application.icon,
|
icon: application.icon,
|
||||||
@@ -222,7 +229,8 @@ class AppSelectorViewModel extends BaseViewModel {
|
|||||||
(app) =>
|
(app) =>
|
||||||
query.isEmpty ||
|
query.isEmpty ||
|
||||||
query.length < 2 ||
|
query.length < 2 ||
|
||||||
app.appName.toLowerCase().contains(query.toLowerCase()),
|
app.appName.toLowerCase().contains(query.toLowerCase()) ||
|
||||||
|
app.packageName.toLowerCase().contains(query.toLowerCase()),
|
||||||
)
|
)
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,10 +34,8 @@ class HomeViewModel extends BaseViewModel {
|
|||||||
final RevancedAPI _revancedAPI = locator<RevancedAPI>();
|
final RevancedAPI _revancedAPI = locator<RevancedAPI>();
|
||||||
final Toast _toast = locator<Toast>();
|
final Toast _toast = locator<Toast>();
|
||||||
final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
|
final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
|
||||||
DateTime? _lastUpdate;
|
|
||||||
bool showUpdatableApps = false;
|
bool showUpdatableApps = false;
|
||||||
List<PatchedApplication> patchedInstalledApps = [];
|
List<PatchedApplication> patchedInstalledApps = [];
|
||||||
List<PatchedApplication> patchedUpdatableApps = [];
|
|
||||||
String? _latestManagerVersion = '';
|
String? _latestManagerVersion = '';
|
||||||
File? downloadedApk;
|
File? downloadedApk;
|
||||||
|
|
||||||
@@ -82,7 +80,7 @@ class HomeViewModel extends BaseViewModel {
|
|||||||
_toast.showBottom('homeView.errorDownloadMessage');
|
_toast.showBottom('homeView.errorDownloadMessage');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_getPatchedApps();
|
|
||||||
_managerAPI.reAssessSavedApps().then((_) => _getPatchedApps());
|
_managerAPI.reAssessSavedApps().then((_) => _getPatchedApps());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,10 +106,6 @@ class HomeViewModel extends BaseViewModel {
|
|||||||
|
|
||||||
void _getPatchedApps() {
|
void _getPatchedApps() {
|
||||||
patchedInstalledApps = _managerAPI.getPatchedApps().toList();
|
patchedInstalledApps = _managerAPI.getPatchedApps().toList();
|
||||||
patchedUpdatableApps = _managerAPI
|
|
||||||
.getPatchedApps()
|
|
||||||
.where((app) => app.hasUpdates == true)
|
|
||||||
.toList();
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -469,11 +463,7 @@ class HomeViewModel extends BaseViewModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> forceRefresh(BuildContext context) async {
|
Future<void> forceRefresh(BuildContext context) async {
|
||||||
await Future.delayed(const Duration(seconds: 1));
|
_managerAPI.clearAllData();
|
||||||
if (_lastUpdate == null ||
|
|
||||||
_lastUpdate!.difference(DateTime.now()).inSeconds > 2) {
|
|
||||||
_managerAPI.clearAllData();
|
|
||||||
}
|
|
||||||
_toast.showBottom('homeView.refreshSuccess');
|
_toast.showBottom('homeView.refreshSuccess');
|
||||||
initialize(context);
|
initialize(context);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,11 +21,23 @@ class InstallerView extends StatelessWidget {
|
|||||||
bottom: false,
|
bottom: false,
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
floatingActionButton: Visibility(
|
floatingActionButton: Visibility(
|
||||||
visible: !model.isPatching,
|
visible: !model.isPatching && !model.hasErrors,
|
||||||
child: FloatingActionButton.extended(
|
child: FloatingActionButton.extended(
|
||||||
label: I18nText('installerView.installButton'),
|
label: I18nText(
|
||||||
icon: const Icon(Icons.file_download_outlined),
|
model.isInstalled
|
||||||
onPressed: () => model.installTypeDialog(context),
|
? 'installerView.openButton'
|
||||||
|
: 'installerView.installButton',
|
||||||
|
),
|
||||||
|
icon: model.isInstalled
|
||||||
|
? const Icon(Icons.open_in_new)
|
||||||
|
: const Icon(Icons.file_download_outlined),
|
||||||
|
onPressed: model.isInstalled
|
||||||
|
? () => {
|
||||||
|
model.openApp(),
|
||||||
|
model.cleanPatcher(),
|
||||||
|
Navigator.of(context).pop(),
|
||||||
|
}
|
||||||
|
: () => model.installTypeDialog(context),
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import 'package:revanced_manager/services/root_api.dart';
|
|||||||
import 'package:revanced_manager/services/toast.dart';
|
import 'package:revanced_manager/services/toast.dart';
|
||||||
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
|
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
|
||||||
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
|
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:stacked/stacked.dart';
|
||||||
import 'package:wakelock/wakelock.dart';
|
import 'package:wakelock/wakelock.dart';
|
||||||
|
|
||||||
@@ -29,6 +31,7 @@ class InstallerViewModel extends BaseViewModel {
|
|||||||
'app.revanced.manager.flutter/installer',
|
'app.revanced.manager.flutter/installer',
|
||||||
);
|
);
|
||||||
final ScrollController scrollController = ScrollController();
|
final ScrollController scrollController = ScrollController();
|
||||||
|
final ScreenshotCallback screenshotCallback = ScreenshotCallback();
|
||||||
double? progress = 0.0;
|
double? progress = 0.0;
|
||||||
String logs = '';
|
String logs = '';
|
||||||
String headerLogs = '';
|
String headerLogs = '';
|
||||||
@@ -38,6 +41,7 @@ class InstallerViewModel extends BaseViewModel {
|
|||||||
bool hasErrors = false;
|
bool hasErrors = false;
|
||||||
bool isCanceled = false;
|
bool isCanceled = false;
|
||||||
bool cancel = false;
|
bool cancel = false;
|
||||||
|
bool showPopupScreenshotWarning = true;
|
||||||
|
|
||||||
Future<void> initialize(BuildContext context) async {
|
Future<void> initialize(BuildContext context) async {
|
||||||
isRooted = await _rootAPI.isRooted();
|
isRooted = await _rootAPI.isRooted();
|
||||||
@@ -64,6 +68,12 @@ class InstallerViewModel extends BaseViewModel {
|
|||||||
} // ignore
|
} // ignore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
screenshotCallback.addListener(() {
|
||||||
|
if (showPopupScreenshotWarning) {
|
||||||
|
showPopupScreenshotWarning = false;
|
||||||
|
screenshotDetected(context);
|
||||||
|
}
|
||||||
|
});
|
||||||
await Wakelock.enable();
|
await Wakelock.enable();
|
||||||
await handlePlatformChannelMethods();
|
await handlePlatformChannelMethods();
|
||||||
await runPatcher();
|
await runPatcher();
|
||||||
@@ -129,29 +139,30 @@ class InstallerViewModel extends BaseViewModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> runPatcher() async {
|
Future<void> runPatcher() async {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
update(0.0, 'Initializing...', 'Initializing installer');
|
await _patcherAPI.runPatcher(
|
||||||
if (_patches.isNotEmpty) {
|
_app.packageName,
|
||||||
try {
|
_app.apkFilePath,
|
||||||
update(0.1, '', 'Creating working directory');
|
_patches,
|
||||||
await _patcherAPI.runPatcher(
|
);
|
||||||
_app.packageName,
|
} on Exception catch (e) {
|
||||||
_app.apkFilePath,
|
update(
|
||||||
_patches,
|
-100.0,
|
||||||
);
|
'Failed...',
|
||||||
} on Exception catch (e) {
|
'Something went wrong:\n$e',
|
||||||
update(
|
);
|
||||||
-100.0,
|
if (kDebugMode) {
|
||||||
'Aborted...',
|
print(e);
|
||||||
'An error occurred! Aborted\nError:\n$e',
|
|
||||||
);
|
|
||||||
if (kDebugMode) {
|
|
||||||
print(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
update(-100.0, 'Aborted...', 'No app or patches selected! Aborted');
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Necessary to reset the state of patches so that they
|
||||||
|
// can be reloaded again.
|
||||||
|
_managerAPI.patches.clear();
|
||||||
|
await _patcherAPI.loadPatches();
|
||||||
|
|
||||||
|
try {
|
||||||
if (FlutterBackground.isBackgroundExecutionEnabled) {
|
if (FlutterBackground.isBackgroundExecutionEnabled) {
|
||||||
try {
|
try {
|
||||||
FlutterBackground.disableBackgroundExecution();
|
FlutterBackground.disableBackgroundExecution();
|
||||||
@@ -169,6 +180,72 @@ class InstallerViewModel extends BaseViewModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> copyLogs() async {
|
||||||
|
final info = await AboutInfo.getInfo();
|
||||||
|
|
||||||
|
final formattedLogs = [
|
||||||
|
'```',
|
||||||
|
'~ Device Info',
|
||||||
|
'ReVanced Manager: ${info['version']}',
|
||||||
|
'Build: ${info['flavor']}',
|
||||||
|
'Model: ${info['model']}',
|
||||||
|
'Android version: ${info['androidVersion']}',
|
||||||
|
'Supported architectures: ${info['supportedArch'].join(", ")}',
|
||||||
|
|
||||||
|
'\n~ Patch Info',
|
||||||
|
'App: ${_app.packageName} v${_app.version}',
|
||||||
|
'Patches version: ${_managerAPI.patchesVersion}',
|
||||||
|
'Patches: ${_patches.map((p) => p.name).toList().join(", ")}',
|
||||||
|
|
||||||
|
'\n~ Settings',
|
||||||
|
'Allow changing patch selection: ${_managerAPI.isPatchesChangeEnabled()}',
|
||||||
|
'Show universal patches: ${_managerAPI.areUniversalPatchesEnabled()}',
|
||||||
|
'Version compatibility check: ${_managerAPI.isVersionCompatibilityCheckEnabled()}',
|
||||||
|
'Patches source: ${_managerAPI.getPatchesRepo()}',
|
||||||
|
'Integration source: ${_managerAPI.getIntegrationsRepo()}',
|
||||||
|
|
||||||
|
'\n~ Logs',
|
||||||
|
logs,
|
||||||
|
'```',
|
||||||
|
];
|
||||||
|
|
||||||
|
Clipboard.setData(ClipboardData(text: formattedLogs.join('\n')));
|
||||||
|
_toast.showBottom('installerView.copiedToClipboard');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> screenshotDetected(BuildContext context) async {
|
||||||
|
await showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: I18nText(
|
||||||
|
'warning',
|
||||||
|
),
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||||
|
icon: const Icon(Icons.warning),
|
||||||
|
content: SingleChildScrollView(
|
||||||
|
child: I18nText('installerView.screenshotDetected'),
|
||||||
|
),
|
||||||
|
actions: <Widget>[
|
||||||
|
CustomMaterialButton(
|
||||||
|
isFilled: false,
|
||||||
|
label: I18nText('noButton'),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
CustomMaterialButton(
|
||||||
|
label: I18nText('yesButton'),
|
||||||
|
onPressed: () {
|
||||||
|
copyLogs();
|
||||||
|
showPopupScreenshotWarning = true;
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> installTypeDialog(BuildContext context) async {
|
Future<void> installTypeDialog(BuildContext context) async {
|
||||||
final ValueNotifier<int> installType = ValueNotifier(0);
|
final ValueNotifier<int> installType = ValueNotifier(0);
|
||||||
if (isRooted) {
|
if (isRooted) {
|
||||||
@@ -182,52 +259,55 @@ class InstallerViewModel extends BaseViewModel {
|
|||||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||||
icon: const Icon(Icons.file_download_outlined),
|
icon: const Icon(Icons.file_download_outlined),
|
||||||
contentPadding: const EdgeInsets.symmetric(vertical: 16),
|
contentPadding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
content: ValueListenableBuilder(
|
content: SingleChildScrollView(
|
||||||
valueListenable: installType,
|
child: ValueListenableBuilder(
|
||||||
builder: (context, value, child) {
|
valueListenable: installType,
|
||||||
return Column(
|
builder: (context, value, child) {
|
||||||
mainAxisSize: MainAxisSize.min,
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
Padding(
|
children: [
|
||||||
padding: const EdgeInsets.symmetric(
|
Padding(
|
||||||
horizontal: 20,
|
padding: const EdgeInsets.symmetric(
|
||||||
vertical: 10,
|
horizontal: 20,
|
||||||
),
|
vertical: 10,
|
||||||
child: I18nText(
|
),
|
||||||
'installerView.installTypeDescription',
|
child: I18nText(
|
||||||
child: Text(
|
'installerView.installTypeDescription',
|
||||||
'',
|
child: Text(
|
||||||
style: TextStyle(
|
'',
|
||||||
fontSize: 16,
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.w500,
|
fontSize: 16,
|
||||||
color: Theme.of(context).colorScheme.secondary,
|
fontWeight: FontWeight.w500,
|
||||||
|
color: Theme.of(context).colorScheme.secondary,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
RadioListTile(
|
||||||
RadioListTile(
|
title: I18nText('installerView.installNonRootType'),
|
||||||
title: I18nText('installerView.installNonRootType'),
|
contentPadding:
|
||||||
subtitle: I18nText('installerView.installRecommendedType'),
|
const EdgeInsets.symmetric(horizontal: 16),
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 10),
|
value: 0,
|
||||||
value: 0,
|
groupValue: value,
|
||||||
groupValue: value,
|
onChanged: (selected) {
|
||||||
onChanged: (selected) {
|
installType.value = selected!;
|
||||||
installType.value = selected!;
|
},
|
||||||
},
|
),
|
||||||
),
|
RadioListTile(
|
||||||
RadioListTile(
|
title: I18nText('installerView.installRootType'),
|
||||||
title: I18nText('installerView.installRootType'),
|
contentPadding:
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 10),
|
const EdgeInsets.symmetric(horizontal: 16),
|
||||||
value: 1,
|
value: 1,
|
||||||
groupValue: value,
|
groupValue: value,
|
||||||
onChanged: (selected) {
|
onChanged: (selected) {
|
||||||
installType.value = selected!;
|
installType.value = selected!;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
CustomMaterialButton(
|
CustomMaterialButton(
|
||||||
@@ -255,9 +335,9 @@ class InstallerViewModel extends BaseViewModel {
|
|||||||
Future<void> stopPatcher() async {
|
Future<void> stopPatcher() async {
|
||||||
try {
|
try {
|
||||||
isCanceled = true;
|
isCanceled = true;
|
||||||
update(0.5, 'Aborting...', 'Canceling patching process');
|
update(0.5, 'Canceling...', 'Canceling patching process');
|
||||||
await _patcherAPI.stopPatcher();
|
await _patcherAPI.stopPatcher();
|
||||||
update(-100.0, 'Aborted...', 'Press back to exit');
|
update(-100.0, 'Canceled...', 'Press back to exit');
|
||||||
} on Exception catch (e) {
|
} on Exception catch (e) {
|
||||||
if (kDebugMode) {
|
if (kDebugMode) {
|
||||||
print(e);
|
print(e);
|
||||||
@@ -268,56 +348,33 @@ class InstallerViewModel extends BaseViewModel {
|
|||||||
Future<void> installResult(BuildContext context, bool installAsRoot) async {
|
Future<void> installResult(BuildContext context, bool installAsRoot) async {
|
||||||
try {
|
try {
|
||||||
_app.isRooted = installAsRoot;
|
_app.isRooted = installAsRoot;
|
||||||
final bool hasMicroG =
|
update(
|
||||||
_patches.any((p) => p.name.endsWith('MicroG support'));
|
1.0,
|
||||||
final bool rootMicroG = installAsRoot && hasMicroG;
|
'Installing...',
|
||||||
final bool rootFromStorage = installAsRoot && _app.isFromStorage;
|
_app.isRooted
|
||||||
final bool ytWithoutRootMicroG =
|
? 'Installing patched file using root method'
|
||||||
!installAsRoot && !hasMicroG && _app.packageName.contains('youtube');
|
: 'Installing patched file using nonroot method',
|
||||||
if (rootMicroG || rootFromStorage || ytWithoutRootMicroG) {
|
);
|
||||||
return showDialog(
|
isInstalled = await _patcherAPI.installPatchedFile(_app);
|
||||||
context: context,
|
if (isInstalled) {
|
||||||
builder: (context) => AlertDialog(
|
_app.isFromStorage = false;
|
||||||
title: I18nText('installerView.installErrorDialogTitle'),
|
_app.patchDate = DateTime.now();
|
||||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
_app.appliedPatches = _patches.map((p) => p.name).toList();
|
||||||
content: I18nText(
|
|
||||||
rootMicroG
|
// In case a patch changed the app name or package name,
|
||||||
? 'installerView.installErrorDialogText1'
|
// update the app info.
|
||||||
: rootFromStorage
|
final app =
|
||||||
? 'installerView.installErrorDialogText3'
|
await DeviceApps.getAppFromStorage(_patcherAPI.outFile!.path);
|
||||||
: 'installerView.installErrorDialogText2',
|
if (app != null) {
|
||||||
),
|
_app.name = app.appName;
|
||||||
actions: <Widget>[
|
_app.packageName = app.packageName;
|
||||||
CustomMaterialButton(
|
|
||||||
label: I18nText('okButton'),
|
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
update(
|
|
||||||
1.0,
|
|
||||||
'Installing...',
|
|
||||||
_app.isRooted
|
|
||||||
? 'Installing patched file using root method'
|
|
||||||
: 'Installing patched file using nonroot method',
|
|
||||||
);
|
|
||||||
isInstalled = await _patcherAPI.installPatchedFile(_app);
|
|
||||||
if (isInstalled) {
|
|
||||||
update(1.0, 'Installed!', 'Installed!');
|
|
||||||
_app.isFromStorage = false;
|
|
||||||
_app.patchDate = DateTime.now();
|
|
||||||
_app.appliedPatches = _patches.map((p) => p.name).toList();
|
|
||||||
if (hasMicroG) {
|
|
||||||
_app.name += ' ReVanced';
|
|
||||||
_app.packageName = _app.packageName.replaceFirst(
|
|
||||||
'com.google.',
|
|
||||||
'app.revanced.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
await _managerAPI.savePatchedApp(_app);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await _managerAPI.savePatchedApp(_app);
|
||||||
|
|
||||||
|
update(1.0, 'Installed!', 'Installed!');
|
||||||
|
} else {
|
||||||
|
// TODO(aabed): Show error message.
|
||||||
}
|
}
|
||||||
} on Exception catch (e) {
|
} on Exception catch (e) {
|
||||||
if (kDebugMode) {
|
if (kDebugMode) {
|
||||||
@@ -336,10 +393,6 @@ class InstallerViewModel extends BaseViewModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void exportLog() {
|
|
||||||
_patcherAPI.exportPatcherLog(logs);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> cleanPatcher() async {
|
Future<void> cleanPatcher() async {
|
||||||
try {
|
try {
|
||||||
_patcherAPI.cleanPatcher();
|
_patcherAPI.cleanPatcher();
|
||||||
@@ -363,7 +416,7 @@ class InstallerViewModel extends BaseViewModel {
|
|||||||
exportResult();
|
exportResult();
|
||||||
break;
|
break;
|
||||||
case 1:
|
case 1:
|
||||||
exportLog();
|
copyLogs();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -385,6 +438,7 @@ class InstallerViewModel extends BaseViewModel {
|
|||||||
} else {
|
} else {
|
||||||
_patcherAPI.cleanPatcher();
|
_patcherAPI.cleanPatcher();
|
||||||
}
|
}
|
||||||
|
screenshotCallback.dispose();
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
// ignore_for_file: use_build_context_synchronously
|
// ignore_for_file: use_build_context_synchronously
|
||||||
|
import 'package:device_info_plus/device_info_plus.dart';
|
||||||
import 'package:dynamic_themes/dynamic_themes.dart';
|
import 'package:dynamic_themes/dynamic_themes.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
@@ -18,25 +19,34 @@ class NavigationViewModel extends IndexTrackingViewModel {
|
|||||||
Future<void> initialize(BuildContext context) async {
|
Future<void> initialize(BuildContext context) async {
|
||||||
locator<Toast>().initialize(context);
|
locator<Toast>().initialize(context);
|
||||||
final SharedPreferences prefs = await SharedPreferences.getInstance();
|
final SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
requestManageExternalStorage();
|
|
||||||
|
|
||||||
if (prefs.getBool('permissionsRequested') == null) {
|
if (prefs.getBool('permissionsRequested') == null) {
|
||||||
await Permission.storage.request();
|
await Permission.storage.request();
|
||||||
await Permission.manageExternalStorage.request();
|
|
||||||
await prefs.setBool('permissionsRequested', true);
|
await prefs.setBool('permissionsRequested', true);
|
||||||
RootAPI().hasRootPermissions().then(
|
await RootAPI().hasRootPermissions().then(
|
||||||
(value) => Permission.requestInstallPackages.request().then(
|
(value) => Permission.requestInstallPackages.request().then(
|
||||||
(value) => Permission.ignoreBatteryOptimizations.request(),
|
(value) => Permission.ignoreBatteryOptimizations.request(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prefs.getBool('useDarkTheme') == null) {
|
final dynamicTheme = DynamicTheme.of(context)!;
|
||||||
final bool isDark =
|
if (prefs.getInt('themeMode') == null) {
|
||||||
MediaQuery.platformBrightnessOf(context) != Brightness.light;
|
await prefs.setInt('themeMode', 0);
|
||||||
await prefs.setBool('useDarkTheme', isDark);
|
await dynamicTheme.setTheme(0);
|
||||||
await DynamicTheme.of(context)!.setTheme(isDark ? 1 : 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Force disable Material You on Android 11 and below
|
||||||
|
if (dynamicTheme.themeId.isOdd) {
|
||||||
|
const int android12SdkVersion = 31;
|
||||||
|
final AndroidDeviceInfo info = await DeviceInfoPlugin().androidInfo;
|
||||||
|
if (info.version.sdkInt < android12SdkVersion) {
|
||||||
|
await prefs.setInt('themeMode', 0);
|
||||||
|
await prefs.setBool('useDynamicTheme', false);
|
||||||
|
await dynamicTheme.setTheme(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
||||||
SystemChrome.setSystemUIOverlayStyle(
|
SystemChrome.setSystemUIOverlayStyle(
|
||||||
SystemUiOverlayStyle(
|
SystemUiOverlayStyle(
|
||||||
@@ -49,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) {
|
Widget getViewForIndex(int index) {
|
||||||
switch (index) {
|
switch (index) {
|
||||||
case 0:
|
case 0:
|
||||||
|
|||||||
129
lib/ui/views/patch_options/patch_options_view.dart
Normal file
129
lib/ui/views/patch_options/patch_options_view.dart
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_i18n/flutter_i18n.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
import 'package:revanced_manager/models/patch.dart';
|
||||||
|
import 'package:revanced_manager/ui/views/patch_options/patch_options_viewmodel.dart';
|
||||||
|
import 'package:revanced_manager/ui/widgets/patchesSelectorView/patch_options_fields.dart';
|
||||||
|
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
|
||||||
|
import 'package:stacked/stacked.dart';
|
||||||
|
|
||||||
|
class PatchOptionsView extends StatelessWidget {
|
||||||
|
const PatchOptionsView({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ViewModelBuilder<PatchOptionsViewModel>.reactive(
|
||||||
|
onViewModelReady: (model) => model.initialize(),
|
||||||
|
viewModelBuilder: () => PatchOptionsViewModel(),
|
||||||
|
builder: (context, model, child) => GestureDetector(
|
||||||
|
onTap: () => FocusScope.of(context).unfocus(),
|
||||||
|
child: Scaffold(
|
||||||
|
floatingActionButton: FloatingActionButton.extended(
|
||||||
|
onPressed: () async {
|
||||||
|
final bool saved = model.saveOptions(context);
|
||||||
|
if (saved && context.mounted) {
|
||||||
|
Navigator.pop(context);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
label: I18nText('patchOptionsView.saveOptions'),
|
||||||
|
icon: const Icon(Icons.save),
|
||||||
|
),
|
||||||
|
body: CustomScrollView(
|
||||||
|
slivers: <Widget>[
|
||||||
|
SliverAppBar(
|
||||||
|
title: I18nText(
|
||||||
|
'patchOptionsView.viewTitle',
|
||||||
|
child: Text(
|
||||||
|
'',
|
||||||
|
style: GoogleFonts.inter(
|
||||||
|
color: Theme.of(context).textTheme.titleLarge!.color,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
model.resetOptions();
|
||||||
|
},
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.history,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
for (final Option option in model.visibleOptions)
|
||||||
|
if (option.optionClassType == 'StringPatchOption' ||
|
||||||
|
option.optionClassType == 'IntPatchOption')
|
||||||
|
IntAndStringPatchOption(
|
||||||
|
patchOption: option,
|
||||||
|
removeOption: (option) {
|
||||||
|
model.removeOption(option);
|
||||||
|
},
|
||||||
|
onChanged: (value, option) {
|
||||||
|
model.modifyOptions(value, option);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
else if (option.optionClassType == 'BooleanPatchOption')
|
||||||
|
BooleanPatchOption(
|
||||||
|
patchOption: option,
|
||||||
|
removeOption: (option) {
|
||||||
|
model.removeOption(option);
|
||||||
|
},
|
||||||
|
onChanged: (value, option) {
|
||||||
|
model.modifyOptions(value, option);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
else if (option.optionClassType ==
|
||||||
|
'StringListPatchOption' ||
|
||||||
|
option.optionClassType == 'IntListPatchOption' ||
|
||||||
|
option.optionClassType == 'LongListPatchOption')
|
||||||
|
IntStringLongListPatchOption(
|
||||||
|
patchOption: option,
|
||||||
|
removeOption: (option) {
|
||||||
|
model.removeOption(option);
|
||||||
|
},
|
||||||
|
onChanged: (value, option) {
|
||||||
|
model.modifyOptions(value, option);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
else
|
||||||
|
UnsupportedPatchOption(
|
||||||
|
patchOption: option,
|
||||||
|
),
|
||||||
|
if (model.visibleOptions.length !=
|
||||||
|
model.options.length) ...[
|
||||||
|
const SizedBox(
|
||||||
|
height: 8,
|
||||||
|
),
|
||||||
|
CustomMaterialButton(
|
||||||
|
onPressed: () {
|
||||||
|
model.showAddOptionDialog(context);
|
||||||
|
},
|
||||||
|
label: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.add),
|
||||||
|
I18nText('patchOptionsView.addOptions'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
const SizedBox(
|
||||||
|
height: 80,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
258
lib/ui/views/patch_options/patch_options_viewmodel.dart
Normal file
258
lib/ui/views/patch_options/patch_options_viewmodel.dart
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_i18n/widgets/I18nText.dart';
|
||||||
|
import 'package:revanced_manager/app/app.locator.dart';
|
||||||
|
import 'package:revanced_manager/models/patch.dart';
|
||||||
|
import 'package:revanced_manager/services/manager_api.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/widgets/shared/custom_card.dart';
|
||||||
|
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
|
||||||
|
import 'package:stacked/stacked.dart';
|
||||||
|
|
||||||
|
class PatchOptionsViewModel extends BaseViewModel {
|
||||||
|
final ManagerAPI _managerAPI = locator<ManagerAPI>();
|
||||||
|
final String selectedApp =
|
||||||
|
locator<PatcherViewModel>().selectedApp!.packageName;
|
||||||
|
List<Option> options = [];
|
||||||
|
List<Option> savedOptions = [];
|
||||||
|
List<Option> visibleOptions = [];
|
||||||
|
|
||||||
|
Future<void> initialize() async {
|
||||||
|
options = getDefaultOptions();
|
||||||
|
for (final Option option in options) {
|
||||||
|
final Option? savedOption = _managerAPI.getPatchOption(
|
||||||
|
selectedApp,
|
||||||
|
_managerAPI.selectedPatch!.name,
|
||||||
|
option.key,
|
||||||
|
);
|
||||||
|
if (savedOption != null) {
|
||||||
|
savedOptions.add(savedOption);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (savedOptions.isNotEmpty) {
|
||||||
|
visibleOptions = [
|
||||||
|
...savedOptions,
|
||||||
|
...options
|
||||||
|
.where(
|
||||||
|
(option) =>
|
||||||
|
option.required &&
|
||||||
|
!savedOptions.any((sOption) => sOption.key == option.key),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
visibleOptions = [
|
||||||
|
...options.where((option) => option.required).toList(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void addOption(Option option) {
|
||||||
|
visibleOptions.add(option);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void removeOption(Option option) {
|
||||||
|
visibleOptions.removeWhere((vOption) => vOption.key == option.key);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool saveOptions(BuildContext context) {
|
||||||
|
final List<Option> requiredNullOptions = [];
|
||||||
|
for (final Option option in options) {
|
||||||
|
if (!visibleOptions.any((vOption) => vOption.key == option.key)) {
|
||||||
|
_managerAPI.clearPatchOption(
|
||||||
|
selectedApp, _managerAPI.selectedPatch!.name, option.key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (final Option option in visibleOptions) {
|
||||||
|
if (option.required && option.value == null) {
|
||||||
|
requiredNullOptions.add(option);
|
||||||
|
} else {
|
||||||
|
_managerAPI.setPatchOption(
|
||||||
|
option, _managerAPI.selectedPatch!.name, selectedApp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (requiredNullOptions.isNotEmpty) {
|
||||||
|
showRequiredOptionNullDialog(
|
||||||
|
context,
|
||||||
|
requiredNullOptions,
|
||||||
|
_managerAPI,
|
||||||
|
selectedApp,
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void modifyOptions(dynamic value, Option option) {
|
||||||
|
final Option modifiedOption = Option(
|
||||||
|
title: option.title,
|
||||||
|
description: option.description,
|
||||||
|
optionClassType: option.optionClassType,
|
||||||
|
value: value,
|
||||||
|
required: option.required,
|
||||||
|
key: option.key,
|
||||||
|
);
|
||||||
|
visibleOptions[visibleOptions
|
||||||
|
.indexWhere((vOption) => vOption.key == option.key)] = modifiedOption;
|
||||||
|
_managerAPI.modifiedOptions
|
||||||
|
.removeWhere((mOption) => mOption.key == option.key);
|
||||||
|
_managerAPI.modifiedOptions.add(modifiedOption);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Option> getDefaultOptions() {
|
||||||
|
final List<Option> defaultOptions = [];
|
||||||
|
for (final option in _managerAPI.options) {
|
||||||
|
final Option defaultOption = Option(
|
||||||
|
title: option.title,
|
||||||
|
description: option.description,
|
||||||
|
optionClassType: option.optionClassType,
|
||||||
|
value: option.value is List ? option.value.toList() : option.value,
|
||||||
|
required: option.required,
|
||||||
|
key: option.key,
|
||||||
|
);
|
||||||
|
defaultOptions.add(defaultOption);
|
||||||
|
}
|
||||||
|
return defaultOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
void resetOptions() {
|
||||||
|
_managerAPI.modifiedOptions.clear();
|
||||||
|
visibleOptions =
|
||||||
|
getDefaultOptions().where((option) => option.required).toList();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> showAddOptionDialog(BuildContext context) async {
|
||||||
|
await showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||||
|
title: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
I18nText(
|
||||||
|
'patchOptionsView.addOptions',
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
color: Theme.of(context).colorScheme.onSecondaryContainer,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
CustomMaterialButton(
|
||||||
|
label: I18nText('okButton'),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
contentPadding: const EdgeInsets.all(8),
|
||||||
|
content: Wrap(
|
||||||
|
spacing: 14,
|
||||||
|
runSpacing: 14,
|
||||||
|
children: options
|
||||||
|
.where(
|
||||||
|
(option) =>
|
||||||
|
!visibleOptions.any((vOption) => vOption.key == option.key),
|
||||||
|
)
|
||||||
|
.map((e) {
|
||||||
|
return CustomCard(
|
||||||
|
padding: const EdgeInsets.all(4),
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||||
|
onTap: () {
|
||||||
|
addOption(e);
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
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.onSecondaryContainer,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> showRequiredOptionNullDialog(
|
||||||
|
BuildContext context,
|
||||||
|
List<Option> options,
|
||||||
|
ManagerAPI managerAPI,
|
||||||
|
String selectedApp,
|
||||||
|
) async {
|
||||||
|
final List<String> optionsTitles = [];
|
||||||
|
for (final option in options) {
|
||||||
|
optionsTitles.add('• ${option.title}');
|
||||||
|
}
|
||||||
|
await showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||||
|
title: I18nText('notice'),
|
||||||
|
actions: [
|
||||||
|
CustomMaterialButton(
|
||||||
|
isFilled: false,
|
||||||
|
label: I18nText(
|
||||||
|
'patchOptionsView.deselectPatch',
|
||||||
|
),
|
||||||
|
onPressed: () async {
|
||||||
|
if (managerAPI.isPatchesChangeEnabled()) {
|
||||||
|
locator<PatcherViewModel>()
|
||||||
|
.selectedPatches
|
||||||
|
.remove(managerAPI.selectedPatch);
|
||||||
|
locator<PatcherViewModel>().notifyListeners();
|
||||||
|
for (final option in options) {
|
||||||
|
managerAPI.clearPatchOption(
|
||||||
|
selectedApp, managerAPI.selectedPatch!.name, option.key);
|
||||||
|
}
|
||||||
|
Navigator.of(context)
|
||||||
|
..pop()
|
||||||
|
..pop()
|
||||||
|
..pop();
|
||||||
|
} else {
|
||||||
|
PatchesSelectorViewModel().showPatchesChangeDialog(context);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
CustomMaterialButton(
|
||||||
|
label: I18nText('okButton'),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
content: I18nText(
|
||||||
|
'patchOptionsView.requiredOptionNull',
|
||||||
|
translationParams: {
|
||||||
|
'options': optionsTitles.join('\n'),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -22,7 +22,14 @@ class PatcherView extends StatelessWidget {
|
|||||||
child: FloatingActionButton.extended(
|
child: FloatingActionButton.extended(
|
||||||
label: I18nText('patcherView.patchButton'),
|
label: I18nText('patcherView.patchButton'),
|
||||||
icon: const Icon(Icons.build),
|
icon: const Icon(Icons.build),
|
||||||
onPressed: () => model.showRemovedPatchesDialog(context),
|
onPressed: () async{
|
||||||
|
if (model.checkRequiredPatchOption(context)) {
|
||||||
|
final bool proceed = model.showRemovedPatchesDialog(context);
|
||||||
|
if (proceed && context.mounted) {
|
||||||
|
model.showArmv7WarningDialog(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
body: CustomScrollView(
|
body: CustomScrollView(
|
||||||
@@ -45,7 +52,10 @@ class PatcherView extends StatelessWidget {
|
|||||||
delegate: SliverChildListDelegate.fixed(
|
delegate: SliverChildListDelegate.fixed(
|
||||||
<Widget>[
|
<Widget>[
|
||||||
AppSelectorCard(
|
AppSelectorCard(
|
||||||
onPressed: () => model.navigateToAppSelector(),
|
onPressed: () => {
|
||||||
|
model.navigateToAppSelector(),
|
||||||
|
model.ctx = context,
|
||||||
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Opacity(
|
Opacity(
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ class PatcherViewModel extends BaseViewModel {
|
|||||||
final ManagerAPI _managerAPI = locator<ManagerAPI>();
|
final ManagerAPI _managerAPI = locator<ManagerAPI>();
|
||||||
final PatcherAPI _patcherAPI = locator<PatcherAPI>();
|
final PatcherAPI _patcherAPI = locator<PatcherAPI>();
|
||||||
PatchedApplication? selectedApp;
|
PatchedApplication? selectedApp;
|
||||||
|
BuildContext? ctx;
|
||||||
List<Patch> selectedPatches = [];
|
List<Patch> selectedPatches = [];
|
||||||
List<String> removedPatches = [];
|
List<String> removedPatches = [];
|
||||||
|
|
||||||
@@ -44,52 +45,9 @@ class PatcherViewModel extends BaseViewModel {
|
|||||||
return selectedApp == null;
|
return selectedApp == null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> isValidPatchConfig() async {
|
bool showRemovedPatchesDialog(BuildContext context) {
|
||||||
final bool needsResourcePatching = await _patcherAPI.needsResourcePatching(
|
|
||||||
selectedPatches,
|
|
||||||
);
|
|
||||||
if (needsResourcePatching && selectedApp != null) {
|
|
||||||
final bool isSplit = await _managerAPI.isSplitApk(selectedApp!);
|
|
||||||
return !isSplit;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> showPatchConfirmationDialog(BuildContext context) async {
|
|
||||||
final bool isValid = await isValidPatchConfig();
|
|
||||||
if (context.mounted) {
|
|
||||||
if (isValid) {
|
|
||||||
showArmv7WarningDialog(context);
|
|
||||||
} else {
|
|
||||||
return showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => AlertDialog(
|
|
||||||
title: I18nText('warning'),
|
|
||||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
|
||||||
content: I18nText('patcherView.splitApkWarningDialogText'),
|
|
||||||
actions: <Widget>[
|
|
||||||
CustomMaterialButton(
|
|
||||||
label: I18nText('noButton'),
|
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
|
||||||
),
|
|
||||||
CustomMaterialButton(
|
|
||||||
label: I18nText('yesButton'),
|
|
||||||
isFilled: false,
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
showArmv7WarningDialog(context);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> showRemovedPatchesDialog(BuildContext context) async {
|
|
||||||
if (removedPatches.isNotEmpty) {
|
if (removedPatches.isNotEmpty) {
|
||||||
return showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
title: I18nText('notice'),
|
title: I18nText('notice'),
|
||||||
@@ -102,21 +60,59 @@ class PatcherViewModel extends BaseViewModel {
|
|||||||
CustomMaterialButton(
|
CustomMaterialButton(
|
||||||
isFilled: false,
|
isFilled: false,
|
||||||
label: I18nText('noButton'),
|
label: I18nText('noButton'),
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
),
|
),
|
||||||
CustomMaterialButton(
|
CustomMaterialButton(
|
||||||
label: I18nText('yesButton'),
|
label: I18nText('yesButton'),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
navigateToInstaller();
|
showArmv7WarningDialog(context);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
return false;
|
||||||
showArmv7WarningDialog(context);
|
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool checkRequiredPatchOption(BuildContext context) {
|
||||||
|
if (getNullRequiredOptions(selectedPatches, selectedApp!.packageName)
|
||||||
|
.isNotEmpty) {
|
||||||
|
showRequiredOptionDialog(context);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void showRequiredOptionDialog([context]) {
|
||||||
|
showDialog(
|
||||||
|
context: context ?? ctx,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: I18nText('notice'),
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||||
|
content: I18nText('patcherView.requiredOptionDialogText'),
|
||||||
|
actions: <Widget>[
|
||||||
|
CustomMaterialButton(
|
||||||
|
isFilled: false,
|
||||||
|
label: I18nText('cancelButton'),
|
||||||
|
onPressed: () => {
|
||||||
|
Navigator.of(context).pop(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
CustomMaterialButton(
|
||||||
|
label: I18nText('okButton'),
|
||||||
|
onPressed: () => {
|
||||||
|
Navigator.pop(context),
|
||||||
|
navigateToPatchesSelector(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> showArmv7WarningDialog(BuildContext context) async {
|
Future<void> showArmv7WarningDialog(BuildContext context) async {
|
||||||
@@ -185,9 +181,9 @@ class PatcherViewModel extends BaseViewModel {
|
|||||||
this.selectedPatches.clear();
|
this.selectedPatches.clear();
|
||||||
removedPatches.clear();
|
removedPatches.clear();
|
||||||
final List<String> selectedPatches =
|
final List<String> selectedPatches =
|
||||||
await _managerAPI.getSelectedPatches(selectedApp!.originalPackageName);
|
await _managerAPI.getSelectedPatches(selectedApp!.packageName);
|
||||||
final List<Patch> patches =
|
final List<Patch> patches =
|
||||||
_patcherAPI.getFilteredPatches(selectedApp!.originalPackageName);
|
_patcherAPI.getFilteredPatches(selectedApp!.packageName);
|
||||||
this
|
this
|
||||||
.selectedPatches
|
.selectedPatches
|
||||||
.addAll(patches.where((patch) => selectedPatches.contains(patch.name)));
|
.addAll(patches.where((patch) => selectedPatches.contains(patch.name)));
|
||||||
@@ -195,7 +191,7 @@ class PatcherViewModel extends BaseViewModel {
|
|||||||
this.selectedPatches.clear();
|
this.selectedPatches.clear();
|
||||||
this.selectedPatches.addAll(patches.where((patch) => !patch.excluded));
|
this.selectedPatches.addAll(patches.where((patch) => !patch.excluded));
|
||||||
}
|
}
|
||||||
if (!_managerAPI.areExperimentalPatchesEnabled()) {
|
if (_managerAPI.isVersionCompatibilityCheckEnabled()) {
|
||||||
this.selectedPatches.removeWhere((patch) => !isPatchSupported(patch));
|
this.selectedPatches.removeWhere((patch) => !isPatchSupported(patch));
|
||||||
}
|
}
|
||||||
if (!_managerAPI.areUniversalPatchesEnabled()) {
|
if (!_managerAPI.areUniversalPatchesEnabled()) {
|
||||||
@@ -203,10 +199,14 @@ class PatcherViewModel extends BaseViewModel {
|
|||||||
.selectedPatches
|
.selectedPatches
|
||||||
.removeWhere((patch) => patch.compatiblePackages.isEmpty);
|
.removeWhere((patch) => patch.compatiblePackages.isEmpty);
|
||||||
}
|
}
|
||||||
final usedPatches = _managerAPI.getUsedPatches(selectedApp!.originalPackageName);
|
final usedPatches = _managerAPI.getUsedPatches(selectedApp!.packageName);
|
||||||
for (final patch in usedPatches){
|
for (final patch in usedPatches) {
|
||||||
if (!patches.any((p) => p.name == patch.name)){
|
if (!patches.any((p) => p.name == patch.name)) {
|
||||||
removedPatches.add('\u2022 ${patch.name}');
|
removedPatches.add('• ${patch.name}');
|
||||||
|
for (final option in patch.options) {
|
||||||
|
_managerAPI.clearPatchOption(
|
||||||
|
selectedApp!.packageName, patch.name, option.key);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import 'package:revanced_manager/app/app.locator.dart';
|
|||||||
import 'package:revanced_manager/services/manager_api.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/views/patches_selector/patches_selector_viewmodel.dart';
|
||||||
import 'package:revanced_manager/ui/widgets/patchesSelectorView/patch_item.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/ui/widgets/shared/search_bar.dart';
|
||||||
import 'package:revanced_manager/utils/check_for_supported_patch.dart';
|
import 'package:revanced_manager/utils/check_for_supported_patch.dart';
|
||||||
import 'package:stacked/stacked.dart';
|
import 'package:stacked/stacked.dart';
|
||||||
@@ -37,7 +36,6 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
|
|||||||
onViewModelReady: (model) => model.initialize(),
|
onViewModelReady: (model) => model.initialize(),
|
||||||
viewModelBuilder: () => PatchesSelectorViewModel(),
|
viewModelBuilder: () => PatchesSelectorViewModel(),
|
||||||
builder: (context, model, child) => Scaffold(
|
builder: (context, model, child) => Scaffold(
|
||||||
resizeToAvoidBottomInset: false,
|
|
||||||
floatingActionButton: Visibility(
|
floatingActionButton: Visibility(
|
||||||
visible: model.patches.isNotEmpty,
|
visible: model.patches.isNotEmpty,
|
||||||
child: FloatingActionButton.extended(
|
child: FloatingActionButton.extended(
|
||||||
@@ -49,8 +47,10 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
|
|||||||
),
|
),
|
||||||
icon: const Icon(Icons.check),
|
icon: const Icon(Icons.check),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
model.selectPatches();
|
if (!model.areRequiredOptionsNull(context)) {
|
||||||
Navigator.of(context).pop();
|
model.selectPatches();
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -61,60 +61,54 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
|
|||||||
floating: true,
|
floating: true,
|
||||||
title: I18nText(
|
title: I18nText(
|
||||||
'patchesSelectorView.viewTitle',
|
'patchesSelectorView.viewTitle',
|
||||||
child: Text(
|
),
|
||||||
'',
|
titleTextStyle: TextStyle(
|
||||||
style: TextStyle(
|
fontSize: 22.0,
|
||||||
color: Theme.of(context).textTheme.titleLarge!.color,
|
color: Theme.of(context).textTheme.titleLarge!.color,
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
Icons.arrow_back,
|
Icons.arrow_back,
|
||||||
color: Theme.of(context).textTheme.titleLarge!.color,
|
color: Theme.of(context).textTheme.titleLarge!.color,
|
||||||
),
|
),
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
onPressed: () {
|
||||||
|
model.resetSelection();
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
FittedBox(
|
Container(
|
||||||
fit: BoxFit.scaleDown,
|
margin: const EdgeInsets.symmetric(vertical: 12),
|
||||||
child: Container(
|
padding: const EdgeInsets.symmetric(horizontal: 6),
|
||||||
margin: const EdgeInsets.only(top: 12, bottom: 12),
|
decoration: BoxDecoration(
|
||||||
padding:
|
color:
|
||||||
const EdgeInsets.symmetric(horizontal: 6, vertical: 6),
|
Theme.of(context).colorScheme.tertiary.withOpacity(0.5),
|
||||||
decoration: BoxDecoration(
|
borderRadius: BorderRadius.circular(6),
|
||||||
color: Theme.of(context)
|
),
|
||||||
.colorScheme
|
alignment: Alignment.center,
|
||||||
.tertiary
|
child: Text(
|
||||||
.withOpacity(0.5),
|
model.patchesVersion!,
|
||||||
borderRadius: BorderRadius.circular(6),
|
style: TextStyle(
|
||||||
),
|
color: Theme.of(context).textTheme.titleLarge!.color,
|
||||||
child: Text(
|
|
||||||
model.patchesVersion!,
|
|
||||||
style: TextStyle(
|
|
||||||
color: Theme.of(context).textTheme.titleLarge!.color,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
CustomPopupMenu(
|
PopupMenuButton(
|
||||||
onSelected: (value) =>
|
onSelected: (value) {
|
||||||
{model.onMenuSelection(value, context)},
|
model.onMenuSelection(value, context);
|
||||||
children: {
|
},
|
||||||
0: I18nText(
|
itemBuilder: (BuildContext context) => <PopupMenuEntry>[
|
||||||
'patchesSelectorView.loadPatchesSelection',
|
PopupMenuItem(
|
||||||
child: const Text(
|
value: 0,
|
||||||
'',
|
child: I18nText(
|
||||||
style: TextStyle(
|
'patchesSelectorView.loadPatchesSelection',
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
},
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
bottom: PreferredSize(
|
bottom: PreferredSize(
|
||||||
preferredSize: const Size.fromHeight(64.0),
|
preferredSize: const Size.fromHeight(66.0),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
vertical: 8.0,
|
vertical: 8.0,
|
||||||
@@ -188,25 +182,123 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
if (model.newPatchExists())
|
||||||
|
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.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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
...model.getQueriedPatches(_query).map(
|
...model.getQueriedPatches(_query).map(
|
||||||
(patch) {
|
(patch) {
|
||||||
if (patch.compatiblePackages.isNotEmpty) {
|
if (patch.compatiblePackages.isNotEmpty) {
|
||||||
return PatchItem(
|
return PatchItem(
|
||||||
name: patch.name,
|
name: patch.name,
|
||||||
simpleName: patch.getSimpleName(),
|
simpleName: patch.getSimpleName(),
|
||||||
description: patch.description,
|
description: patch.description ?? '',
|
||||||
packageVersion: model.getAppInfo().version,
|
packageVersion: model.getAppInfo().version,
|
||||||
supportedPackageVersions:
|
supportedPackageVersions:
|
||||||
model.getSupportedVersions(patch),
|
model.getSupportedVersions(patch),
|
||||||
isUnsupported: !isPatchSupported(patch),
|
isUnsupported: !isPatchSupported(patch),
|
||||||
isChangeEnabled: _managerAPI.isPatchesChangeEnabled(),
|
isChangeEnabled:
|
||||||
isNew: model.isPatchNew(
|
_managerAPI.isPatchesChangeEnabled(),
|
||||||
|
hasUnsupportedPatchOption:
|
||||||
|
hasUnsupportedRequiredOption(
|
||||||
|
patch.options,
|
||||||
patch,
|
patch,
|
||||||
model.getAppInfo().packageName,
|
|
||||||
),
|
),
|
||||||
|
options: patch.options,
|
||||||
isSelected: model.isSelected(patch),
|
isSelected: model.isSelected(patch),
|
||||||
onChanged: (value) =>
|
navigateToOptions: (options) =>
|
||||||
model.selectPatch(patch, value, context),
|
model.navigateToPatchOptions(
|
||||||
|
options,
|
||||||
|
patch,
|
||||||
|
),
|
||||||
|
onChanged: (value) => model.selectPatch(
|
||||||
|
patch,
|
||||||
|
value,
|
||||||
|
context,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return Container();
|
return Container();
|
||||||
@@ -221,8 +313,23 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
|
|||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
vertical: 10.0,
|
vertical: 10.0,
|
||||||
),
|
),
|
||||||
child: I18nText(
|
child: Container(
|
||||||
'patchesSelectorView.universalPatches',
|
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.getQueriedPatches(_query).map((patch) {
|
...model.getQueriedPatches(_query).map((patch) {
|
||||||
@@ -230,15 +337,26 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
|
|||||||
return PatchItem(
|
return PatchItem(
|
||||||
name: patch.name,
|
name: patch.name,
|
||||||
simpleName: patch.getSimpleName(),
|
simpleName: patch.getSimpleName(),
|
||||||
description: patch.description,
|
description: patch.description ?? '',
|
||||||
packageVersion:
|
packageVersion:
|
||||||
model.getAppInfo().version,
|
model.getAppInfo().version,
|
||||||
supportedPackageVersions:
|
supportedPackageVersions:
|
||||||
model.getSupportedVersions(patch),
|
model.getSupportedVersions(patch),
|
||||||
isUnsupported: !isPatchSupported(patch),
|
isUnsupported: !isPatchSupported(patch),
|
||||||
isChangeEnabled: _managerAPI.isPatchesChangeEnabled(),
|
isChangeEnabled:
|
||||||
isNew: false,
|
_managerAPI.isPatchesChangeEnabled(),
|
||||||
|
hasUnsupportedPatchOption:
|
||||||
|
hasUnsupportedRequiredOption(
|
||||||
|
patch.options,
|
||||||
|
patch,
|
||||||
|
),
|
||||||
|
options: patch.options,
|
||||||
isSelected: model.isSelected(patch),
|
isSelected: model.isSelected(patch),
|
||||||
|
navigateToOptions: (options) =>
|
||||||
|
model.navigateToPatchOptions(
|
||||||
|
options,
|
||||||
|
patch,
|
||||||
|
),
|
||||||
onChanged: (value) => model.selectPatch(
|
onChanged: (value) => model.selectPatch(
|
||||||
patch,
|
patch,
|
||||||
value,
|
value,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'package:collection/collection.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_i18n/widgets/I18nText.dart';
|
import 'package:flutter_i18n/widgets/I18nText.dart';
|
||||||
import 'package:revanced_manager/app/app.locator.dart';
|
import 'package:revanced_manager/app/app.locator.dart';
|
||||||
|
import 'package:revanced_manager/app/app.router.dart';
|
||||||
import 'package:revanced_manager/models/patch.dart';
|
import 'package:revanced_manager/models/patch.dart';
|
||||||
import 'package:revanced_manager/models/patched_application.dart';
|
import 'package:revanced_manager/models/patched_application.dart';
|
||||||
import 'package:revanced_manager/services/manager_api.dart';
|
import 'package:revanced_manager/services/manager_api.dart';
|
||||||
@@ -11,11 +12,14 @@ import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
|
|||||||
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
|
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
|
||||||
import 'package:revanced_manager/utils/check_for_supported_patch.dart';
|
import 'package:revanced_manager/utils/check_for_supported_patch.dart';
|
||||||
import 'package:stacked/stacked.dart';
|
import 'package:stacked/stacked.dart';
|
||||||
|
import 'package:stacked_services/stacked_services.dart';
|
||||||
|
|
||||||
class PatchesSelectorViewModel extends BaseViewModel {
|
class PatchesSelectorViewModel extends BaseViewModel {
|
||||||
final PatcherAPI _patcherAPI = locator<PatcherAPI>();
|
final PatcherAPI _patcherAPI = locator<PatcherAPI>();
|
||||||
final ManagerAPI _managerAPI = locator<ManagerAPI>();
|
final ManagerAPI _managerAPI = locator<ManagerAPI>();
|
||||||
|
final NavigationService _navigationService = locator<NavigationService>();
|
||||||
final List<Patch> patches = [];
|
final List<Patch> patches = [];
|
||||||
|
final List<Patch> currentSelection = [];
|
||||||
final List<Patch> selectedPatches =
|
final List<Patch> selectedPatches =
|
||||||
locator<PatcherViewModel>().selectedPatches;
|
locator<PatcherViewModel>().selectedPatches;
|
||||||
PatchedApplication? selectedApp = locator<PatcherViewModel>().selectedApp;
|
PatchedApplication? selectedApp = locator<PatcherViewModel>().selectedApp;
|
||||||
@@ -28,17 +32,21 @@ class PatchesSelectorViewModel extends BaseViewModel {
|
|||||||
getPatchesVersion().whenComplete(() => notifyListeners());
|
getPatchesVersion().whenComplete(() => notifyListeners());
|
||||||
patches.addAll(
|
patches.addAll(
|
||||||
_patcherAPI.getFilteredPatches(
|
_patcherAPI.getFilteredPatches(
|
||||||
selectedApp!.originalPackageName,
|
selectedApp!.packageName,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
final List<Option> requiredNullOptions =
|
||||||
|
getNullRequiredOptions(patches, selectedApp!.packageName);
|
||||||
patches.sort((a, b) {
|
patches.sort((a, b) {
|
||||||
if (isPatchNew(a, selectedApp!.packageName) ==
|
if (b.options.any((option) => requiredNullOptions.contains(option)) &&
|
||||||
isPatchNew(b, selectedApp!.packageName)) {
|
a.options.isEmpty) {
|
||||||
return a.name.compareTo(b.name);
|
return 1;
|
||||||
} else {
|
} else {
|
||||||
return isPatchNew(b, selectedApp!.packageName) ? 1 : -1;
|
return a.name.compareTo(b.name);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
currentSelection.clear();
|
||||||
|
currentSelection.addAll(selectedPatches);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,6 +56,60 @@ class PatchesSelectorViewModel extends BaseViewModel {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void navigateToPatchOptions(List<Option> setOptions, Patch patch) {
|
||||||
|
_managerAPI.options = setOptions;
|
||||||
|
_managerAPI.selectedPatch = patch;
|
||||||
|
_managerAPI.modifiedOptions.clear();
|
||||||
|
_navigationService.navigateToPatchOptionsView();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool areRequiredOptionsNull(BuildContext context) {
|
||||||
|
final List<String> patchesWithNullRequiredOptions = [];
|
||||||
|
final List<Option> requiredNullOptions =
|
||||||
|
getNullRequiredOptions(selectedPatches, selectedApp!.packageName);
|
||||||
|
if (requiredNullOptions.isNotEmpty) {
|
||||||
|
for (final patch in selectedPatches) {
|
||||||
|
for (final patchOption in patch.options) {
|
||||||
|
if (requiredNullOptions.contains(patchOption)) {
|
||||||
|
patchesWithNullRequiredOptions.add(patch.name);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
showSetRequiredOption(context, patchesWithNullRequiredOptions);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> showSetRequiredOption(
|
||||||
|
BuildContext context,
|
||||||
|
List<String> patches,
|
||||||
|
) async {
|
||||||
|
return showDialog(
|
||||||
|
barrierDismissible: false,
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: I18nText('notice'),
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||||
|
content: I18nText(
|
||||||
|
'patchesSelectorView.setRequiredOption',
|
||||||
|
translationParams: {
|
||||||
|
'patches': patches.map((patch) => '• $patch').join('\n'),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
actions: <Widget>[
|
||||||
|
CustomMaterialButton(
|
||||||
|
label: I18nText('okButton'),
|
||||||
|
onPressed: () => {
|
||||||
|
Navigator.of(context).pop(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
void selectPatch(Patch patch, bool isSelected, BuildContext context) {
|
void selectPatch(Patch patch, bool isSelected, BuildContext context) {
|
||||||
if (_managerAPI.isPatchesChangeEnabled()) {
|
if (_managerAPI.isPatchesChangeEnabled()) {
|
||||||
if (isSelected && !selectedPatches.contains(patch)) {
|
if (isSelected && !selectedPatches.contains(patch)) {
|
||||||
@@ -98,16 +160,16 @@ class PatchesSelectorViewModel extends BaseViewModel {
|
|||||||
|
|
||||||
void selectDefaultPatches() {
|
void selectDefaultPatches() {
|
||||||
selectedPatches.clear();
|
selectedPatches.clear();
|
||||||
if (locator<PatcherViewModel>().selectedApp?.originalPackageName != null) {
|
if (locator<PatcherViewModel>().selectedApp?.packageName != null) {
|
||||||
selectedPatches.addAll(
|
selectedPatches.addAll(
|
||||||
_patcherAPI
|
_patcherAPI
|
||||||
.getFilteredPatches(
|
.getFilteredPatches(
|
||||||
locator<PatcherViewModel>().selectedApp!.originalPackageName,
|
locator<PatcherViewModel>().selectedApp!.packageName,
|
||||||
)
|
)
|
||||||
.where(
|
.where(
|
||||||
(element) =>
|
(element) =>
|
||||||
!element.excluded &&
|
!element.excluded &&
|
||||||
(_managerAPI.areExperimentalPatchesEnabled() ||
|
(!_managerAPI.isVersionCompatibilityCheckEnabled() ||
|
||||||
isPatchSupported(element)),
|
isPatchSupported(element)),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -123,9 +185,19 @@ class PatchesSelectorViewModel extends BaseViewModel {
|
|||||||
void selectPatches() {
|
void selectPatches() {
|
||||||
locator<PatcherViewModel>().selectedPatches = selectedPatches;
|
locator<PatcherViewModel>().selectedPatches = selectedPatches;
|
||||||
saveSelectedPatches();
|
saveSelectedPatches();
|
||||||
|
if (_managerAPI.ctx != null) {
|
||||||
|
Navigator.pop(_managerAPI.ctx!);
|
||||||
|
_managerAPI.ctx = null;
|
||||||
|
}
|
||||||
locator<PatcherViewModel>().notifyListeners();
|
locator<PatcherViewModel>().notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void resetSelection() {
|
||||||
|
selectedPatches.clear();
|
||||||
|
selectedPatches.addAll(currentSelection);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> getPatchesVersion() async {
|
Future<void> getPatchesVersion() async {
|
||||||
patchesVersion = await _managerAPI.getCurrentPatchesVersion();
|
patchesVersion = await _managerAPI.getCurrentPatchesVersion();
|
||||||
}
|
}
|
||||||
@@ -137,7 +209,10 @@ class PatchesSelectorViewModel extends BaseViewModel {
|
|||||||
query.isEmpty ||
|
query.isEmpty ||
|
||||||
query.length < 2 ||
|
query.length < 2 ||
|
||||||
patch.name.toLowerCase().contains(query.toLowerCase()) ||
|
patch.name.toLowerCase().contains(query.toLowerCase()) ||
|
||||||
patch.getSimpleName().toLowerCase().contains(query.toLowerCase()),
|
patch.name
|
||||||
|
.replaceAll(RegExp(r'[^\w\s]+'), '')
|
||||||
|
.toLowerCase()
|
||||||
|
.contains(query.toLowerCase()),
|
||||||
)
|
)
|
||||||
.toList();
|
.toList();
|
||||||
if (_managerAPI.areUniversalPatchesEnabled()) {
|
if (_managerAPI.areUniversalPatchesEnabled()) {
|
||||||
@@ -153,8 +228,9 @@ class PatchesSelectorViewModel extends BaseViewModel {
|
|||||||
return locator<PatcherViewModel>().selectedApp!;
|
return locator<PatcherViewModel>().selectedApp!;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isPatchNew(Patch patch, String packageName) {
|
bool isPatchNew(Patch patch) {
|
||||||
final List<Patch> savedPatches = _managerAPI.getSavedPatches(packageName);
|
final List<Patch> savedPatches =
|
||||||
|
_managerAPI.getSavedPatches(selectedApp!.packageName);
|
||||||
if (savedPatches.isEmpty) {
|
if (savedPatches.isEmpty) {
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
@@ -163,6 +239,12 @@ class PatchesSelectorViewModel extends BaseViewModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool newPatchExists() {
|
||||||
|
return patches.any(
|
||||||
|
(patch) => isPatchNew(patch),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
List<String> getSupportedVersions(Patch patch) {
|
List<String> getSupportedVersions(Patch patch) {
|
||||||
final PatchedApplication app = locator<PatcherViewModel>().selectedApp!;
|
final PatchedApplication app = locator<PatcherViewModel>().selectedApp!;
|
||||||
final Package? package = patch.compatiblePackages.firstWhereOrNull(
|
final Package? package = patch.compatiblePackages.firstWhereOrNull(
|
||||||
@@ -187,7 +269,7 @@ class PatchesSelectorViewModel extends BaseViewModel {
|
|||||||
final List<String> selectedPatches =
|
final List<String> selectedPatches =
|
||||||
this.selectedPatches.map((patch) => patch.name).toList();
|
this.selectedPatches.map((patch) => patch.name).toList();
|
||||||
await _managerAPI.setSelectedPatches(
|
await _managerAPI.setSelectedPatches(
|
||||||
locator<PatcherViewModel>().selectedApp!.originalPackageName,
|
locator<PatcherViewModel>().selectedApp!.packageName,
|
||||||
selectedPatches,
|
selectedPatches,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -195,14 +277,14 @@ class PatchesSelectorViewModel extends BaseViewModel {
|
|||||||
Future<void> loadSelectedPatches(BuildContext context) async {
|
Future<void> loadSelectedPatches(BuildContext context) async {
|
||||||
if (_managerAPI.isPatchesChangeEnabled()) {
|
if (_managerAPI.isPatchesChangeEnabled()) {
|
||||||
final List<String> selectedPatches = await _managerAPI.getSelectedPatches(
|
final List<String> selectedPatches = await _managerAPI.getSelectedPatches(
|
||||||
locator<PatcherViewModel>().selectedApp!.originalPackageName,
|
locator<PatcherViewModel>().selectedApp!.packageName,
|
||||||
);
|
);
|
||||||
if (selectedPatches.isNotEmpty) {
|
if (selectedPatches.isNotEmpty) {
|
||||||
this.selectedPatches.clear();
|
this.selectedPatches.clear();
|
||||||
this.selectedPatches.addAll(
|
this.selectedPatches.addAll(
|
||||||
patches.where((patch) => selectedPatches.contains(patch.name)),
|
patches.where((patch) => selectedPatches.contains(patch.name)),
|
||||||
);
|
);
|
||||||
if (!_managerAPI.areExperimentalPatchesEnabled()) {
|
if (_managerAPI.isVersionCompatibilityCheckEnabled()) {
|
||||||
this.selectedPatches.removeWhere((patch) => !isPatchSupported(patch));
|
this.selectedPatches.removeWhere((patch) => !isPatchSupported(patch));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import 'package:revanced_manager/app/app.locator.dart';
|
|||||||
import 'package:revanced_manager/services/manager_api.dart';
|
import 'package:revanced_manager/services/manager_api.dart';
|
||||||
import 'package:revanced_manager/ui/views/settings/settings_viewmodel.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/settingsView/settings_section.dart';
|
||||||
|
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
|
||||||
import 'package:stacked/stacked.dart';
|
import 'package:stacked/stacked.dart';
|
||||||
|
|
||||||
final _settingViewModel = SettingsViewModel();
|
final _settingViewModel = SettingsViewModel();
|
||||||
@@ -24,37 +25,114 @@ class SUpdateTheme extends BaseViewModel {
|
|||||||
|
|
||||||
Future<void> setUseDynamicTheme(BuildContext context, bool value) async {
|
Future<void> setUseDynamicTheme(BuildContext context, bool value) async {
|
||||||
await _managerAPI.setUseDynamicTheme(value);
|
await _managerAPI.setUseDynamicTheme(value);
|
||||||
final int currentTheme = DynamicTheme.of(context)!.themeId;
|
final int currentTheme = (DynamicTheme.of(context)!.themeId ~/ 2) * 2;
|
||||||
if (currentTheme.isEven) {
|
await DynamicTheme.of(context)!.setTheme(currentTheme + (value ? 1 : 0));
|
||||||
await DynamicTheme.of(context)!.setTheme(value ? 2 : 0);
|
|
||||||
} else {
|
|
||||||
await DynamicTheme.of(context)!.setTheme(value ? 3 : 1);
|
|
||||||
}
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool getDarkThemeStatus() {
|
int getThemeMode() {
|
||||||
return _managerAPI.getUseDarkTheme();
|
return _managerAPI.getThemeMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> setUseDarkTheme(BuildContext context, bool value) async {
|
Future<void> setThemeMode(BuildContext context, int value) async {
|
||||||
await _managerAPI.setUseDarkTheme(value);
|
await _managerAPI.setThemeMode(value);
|
||||||
final int currentTheme = DynamicTheme.of(context)!.themeId;
|
final bool isDynamicTheme = DynamicTheme.of(context)!.themeId.isEven;
|
||||||
if (currentTheme < 2) {
|
await DynamicTheme.of(context)!.setTheme(value * 2 + (isDynamicTheme ? 0 : 1));
|
||||||
await DynamicTheme.of(context)!.setTheme(value ? 1 : 0);
|
final bool isLight = value != 2 && (value == 1 || DynamicTheme.of(context)!.theme.brightness == Brightness.light);
|
||||||
} else {
|
|
||||||
await DynamicTheme.of(context)!.setTheme(value ? 3 : 2);
|
|
||||||
}
|
|
||||||
SystemChrome.setSystemUIOverlayStyle(
|
SystemChrome.setSystemUIOverlayStyle(
|
||||||
SystemUiOverlayStyle(
|
SystemUiOverlayStyle(
|
||||||
systemNavigationBarIconBrightness:
|
systemNavigationBarIconBrightness:
|
||||||
value ? Brightness.light : Brightness.dark,
|
isLight ? Brightness.dark : Brightness.light,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
I18nText getThemeModeName() {
|
||||||
|
switch (getThemeMode()) {
|
||||||
|
case 0:
|
||||||
|
return I18nText('settingsView.systemThemeLabel');
|
||||||
|
case 1:
|
||||||
|
return I18nText('settingsView.lightThemeLabel');
|
||||||
|
case 2:
|
||||||
|
return I18nText('settingsView.darkThemeLabel');
|
||||||
|
default:
|
||||||
|
return I18nText('settingsView.systemThemeLabel');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> showThemeDialog(BuildContext context) async {
|
||||||
|
final ValueNotifier<int> newTheme = ValueNotifier(getThemeMode());
|
||||||
|
|
||||||
|
return showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: I18nText('settingsView.themeModeLabel'),
|
||||||
|
icon: const Icon(Icons.palette),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||||
|
content: SingleChildScrollView(
|
||||||
|
child: ValueListenableBuilder(
|
||||||
|
valueListenable: newTheme,
|
||||||
|
builder: (context, value, child) {
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
RadioListTile(
|
||||||
|
title: I18nText('settingsView.systemThemeLabel'),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
value: 0,
|
||||||
|
groupValue: value,
|
||||||
|
onChanged: (value) {
|
||||||
|
newTheme.value = value!;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
RadioListTile(
|
||||||
|
title: I18nText('settingsView.lightThemeLabel'),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
value: 1,
|
||||||
|
groupValue: value,
|
||||||
|
onChanged: (value) {
|
||||||
|
newTheme.value = value!;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
RadioListTile(
|
||||||
|
title: I18nText('settingsView.darkThemeLabel'),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
value: 2,
|
||||||
|
groupValue: value,
|
||||||
|
onChanged: (value) {
|
||||||
|
newTheme.value = value!;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: <Widget>[
|
||||||
|
CustomMaterialButton(
|
||||||
|
isFilled: false,
|
||||||
|
label: I18nText('cancelButton'),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
CustomMaterialButton(
|
||||||
|
label: I18nText('okButton'),
|
||||||
|
onPressed: () {
|
||||||
|
setThemeMode(context, newTheme.value);
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final sUpdateTheme = SUpdateTheme();
|
||||||
class SUpdateThemeUI extends StatelessWidget {
|
class SUpdateThemeUI extends StatelessWidget {
|
||||||
const SUpdateThemeUI({super.key});
|
const SUpdateThemeUI({super.key});
|
||||||
|
|
||||||
@@ -63,10 +141,10 @@ class SUpdateThemeUI extends StatelessWidget {
|
|||||||
return SettingsSection(
|
return SettingsSection(
|
||||||
title: 'settingsView.appearanceSectionTitle',
|
title: 'settingsView.appearanceSectionTitle',
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
SwitchListTile(
|
ListTile(
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||||
title: I18nText(
|
title: I18nText(
|
||||||
'settingsView.darkThemeLabel',
|
'settingsView.themeModeLabel',
|
||||||
child: const Text(
|
child: const Text(
|
||||||
'',
|
'',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
@@ -75,12 +153,11 @@ class SUpdateThemeUI extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
subtitle: I18nText('settingsView.darkThemeHint'),
|
trailing: CustomMaterialButton(
|
||||||
value: SUpdateTheme().getDarkThemeStatus(),
|
label: sUpdateTheme.getThemeModeName(),
|
||||||
onChanged: (value) => SUpdateTheme().setUseDarkTheme(
|
onPressed: () => { sUpdateTheme.showThemeDialog(context) },
|
||||||
context,
|
|
||||||
value,
|
|
||||||
),
|
),
|
||||||
|
onTap: () => { sUpdateTheme.showThemeDialog(context) },
|
||||||
),
|
),
|
||||||
FutureBuilder<int>(
|
FutureBuilder<int>(
|
||||||
future: _settingViewModel.getSdkVersion(),
|
future: _settingViewModel.getSdkVersion(),
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ 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/settingsFragment/settings_update_theme.dart';
|
||||||
import 'package:revanced_manager/ui/views/settings/settings_viewmodel.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_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_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/settingsView/settings_team_section.dart';
|
||||||
import 'package:revanced_manager/ui/widgets/shared/custom_sliver_app_bar.dart';
|
import 'package:revanced_manager/ui/widgets/shared/custom_sliver_app_bar.dart';
|
||||||
import 'package:stacked/stacked.dart';
|
import 'package:stacked/stacked.dart';
|
||||||
@@ -40,16 +40,24 @@ class SettingsView extends StatelessWidget {
|
|||||||
SliverList(
|
SliverList(
|
||||||
delegate: SliverChildListDelegate.fixed(
|
delegate: SliverChildListDelegate.fixed(
|
||||||
<Widget>[
|
<Widget>[
|
||||||
SUpdateThemeUI(),
|
ListView(
|
||||||
// SUpdateLanguageUI(),
|
padding: EdgeInsets.zero,
|
||||||
// _settingsDivider,
|
shrinkWrap: true,
|
||||||
STeamSection(),
|
physics: NeverScrollableScrollPhysics(),
|
||||||
_settingsDivider,
|
children: const [
|
||||||
SAdvancedSection(),
|
SUpdateThemeUI(),
|
||||||
_settingsDivider,
|
// _settingsDivider,
|
||||||
SExportSection(),
|
// SUpdateLanguageUI(),
|
||||||
_settingsDivider,
|
_settingsDivider,
|
||||||
SInfoSection(),
|
SAdvancedSection(),
|
||||||
|
_settingsDivider,
|
||||||
|
SExportSection(),
|
||||||
|
_settingsDivider,
|
||||||
|
STeamSection(),
|
||||||
|
_settingsDivider,
|
||||||
|
SDebugSection(),
|
||||||
|
],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -71,12 +71,6 @@ class SettingsViewModel extends BaseViewModel {
|
|||||||
actions: [
|
actions: [
|
||||||
CustomMaterialButton(
|
CustomMaterialButton(
|
||||||
isFilled: false,
|
isFilled: false,
|
||||||
label: I18nText('noButton'),
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
CustomMaterialButton(
|
|
||||||
label: I18nText('yesButton'),
|
label: I18nText('yesButton'),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
_managerAPI.setChangingToggleModified(true);
|
_managerAPI.setChangingToggleModified(true);
|
||||||
@@ -84,6 +78,12 @@ class SettingsViewModel extends BaseViewModel {
|
|||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
CustomMaterialButton(
|
||||||
|
label: I18nText('noButton'),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -135,12 +135,12 @@ class SettingsViewModel extends BaseViewModel {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool areExperimentalPatchesEnabled() {
|
bool isVersionCompatibilityCheckEnabled() {
|
||||||
return _managerAPI.areExperimentalPatchesEnabled();
|
return _managerAPI.isVersionCompatibilityCheckEnabled();
|
||||||
}
|
}
|
||||||
|
|
||||||
void useExperimentalPatches(bool value) {
|
void useVersionCompatibilityCheck(bool value) {
|
||||||
_managerAPI.enableExperimentalPatchesStatus(value);
|
_managerAPI.enableVersionCompatibilityCheckStatus(value);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,6 +246,11 @@ class SettingsViewModel extends BaseViewModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void resetAllOptions() {
|
||||||
|
_managerAPI.resetAllOptions();
|
||||||
|
_toast.showBottom('settingsView.resetStoredOptions');
|
||||||
|
}
|
||||||
|
|
||||||
void resetSelectedPatches() {
|
void resetSelectedPatches() {
|
||||||
_managerAPI.resetLastSelectedPatches();
|
_managerAPI.resetLastSelectedPatches();
|
||||||
_toast.showBottom('settingsView.resetStoredPatches');
|
_toast.showBottom('settingsView.resetStoredPatches');
|
||||||
|
|||||||
@@ -222,22 +222,6 @@ class AppInfoView extends StatelessWidget {
|
|||||||
subtitle: Text(app.packageName),
|
subtitle: Text(app.packageName),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
ListTile(
|
|
||||||
contentPadding:
|
|
||||||
const EdgeInsets.symmetric(horizontal: 20.0),
|
|
||||||
title: I18nText(
|
|
||||||
'appInfoView.originalPackageNameLabel',
|
|
||||||
child: const Text(
|
|
||||||
'',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 20,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
subtitle: Text(app.originalPackageName),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
ListTile(
|
ListTile(
|
||||||
contentPadding:
|
contentPadding:
|
||||||
const EdgeInsets.symmetric(horizontal: 20.0),
|
const EdgeInsets.symmetric(horizontal: 20.0),
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import 'package:revanced_manager/ui/views/home/home_viewmodel.dart';
|
|||||||
import 'package:revanced_manager/ui/views/navigation/navigation_viewmodel.dart';
|
import 'package:revanced_manager/ui/views/navigation/navigation_viewmodel.dart';
|
||||||
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
|
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
|
||||||
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
|
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
|
||||||
import 'package:revanced_manager/utils/string.dart';
|
|
||||||
import 'package:stacked/stacked.dart';
|
import 'package:stacked/stacked.dart';
|
||||||
|
|
||||||
class AppInfoViewModel extends BaseViewModel {
|
class AppInfoViewModel extends BaseViewModel {
|
||||||
@@ -147,17 +146,7 @@ class AppInfoViewModel extends BaseViewModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String getAppliedPatchesString(List<String> appliedPatches) {
|
String getAppliedPatchesString(List<String> appliedPatches) {
|
||||||
final List<String> names = appliedPatches
|
return '• ${appliedPatches.join('\n• ')}';
|
||||||
.map(
|
|
||||||
(p) => p
|
|
||||||
.replaceAll('-', ' ')
|
|
||||||
.split('-')
|
|
||||||
.join(' ')
|
|
||||||
.toTitleCase()
|
|
||||||
.replaceFirst('Microg', 'MicroG'),
|
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
return '\u2022 ${names.join('\n\u2022 ')}';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void openApp(PatchedApplication app) {
|
void openApp(PatchedApplication app) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_cache_manager/file.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: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:revanced_manager/ui/widgets/shared/custom_card.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ class _ContributorsCardState extends State<ContributorsCard> {
|
|||||||
mode: LaunchMode.externalApplication,
|
mode: LaunchMode.externalApplication,
|
||||||
),
|
),
|
||||||
child: FutureBuilder<File?>(
|
child: FutureBuilder<File?>(
|
||||||
future: DefaultCacheManager().getSingleFile(
|
future: DownloadManager().getSingleFile(
|
||||||
widget.contributors[index]['avatar_url'],
|
widget.contributors[index]['avatar_url'],
|
||||||
),
|
),
|
||||||
builder: (context, snapshot) => snapshot.hasData
|
builder: (context, snapshot) => snapshot.hasData
|
||||||
|
|||||||
@@ -79,8 +79,6 @@ class InstalledAppsCard extends StatelessWidget {
|
|||||||
icon: app.icon,
|
icon: app.icon,
|
||||||
name: app.name,
|
name: app.name,
|
||||||
patchDate: app.patchDate,
|
patchDate: app.patchDate,
|
||||||
changelog: app.changelog,
|
|
||||||
isUpdatableApp: false,
|
|
||||||
onPressed: () =>
|
onPressed: () =>
|
||||||
locator<HomeViewModel>().navigateToAppInfo(app),
|
locator<HomeViewModel>().navigateToAppInfo(app),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ class _LatestCommitCardState extends State<LatestCommitCard> {
|
|||||||
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// ReVanced Patches
|
// Patches
|
||||||
CustomCard(
|
CustomCard(
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
@@ -84,7 +84,7 @@ class _LatestCommitCardState extends State<LatestCommitCard> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
const Text('ReVanced Patches'),
|
const Text('Patches'),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Row(
|
Row(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
|
|||||||
@@ -58,8 +58,10 @@ class PatchSelectorCard extends StatelessWidget {
|
|||||||
|
|
||||||
String _getPatchesSelection() {
|
String _getPatchesSelection() {
|
||||||
String text = '';
|
String text = '';
|
||||||
for (final Patch p in locator<PatcherViewModel>().selectedPatches) {
|
final List<Patch> selectedPatches = locator<PatcherViewModel>().selectedPatches;
|
||||||
text += '\u2022 ${p.getSimpleName()}\n';
|
selectedPatches.sort((a, b) => a.name.compareTo(b.name));
|
||||||
|
for (final Patch p in selectedPatches) {
|
||||||
|
text += '• ${p.getSimpleName()}\n';
|
||||||
}
|
}
|
||||||
return text.substring(0, text.length - 1);
|
return text.substring(0, text.length - 1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_i18n/flutter_i18n.dart';
|
import 'package:flutter_i18n/flutter_i18n.dart';
|
||||||
import 'package:revanced_manager/app/app.locator.dart';
|
import 'package:revanced_manager/app/app.locator.dart';
|
||||||
|
import 'package:revanced_manager/models/patch.dart';
|
||||||
import 'package:revanced_manager/services/manager_api.dart';
|
import 'package:revanced_manager/services/manager_api.dart';
|
||||||
import 'package:revanced_manager/services/toast.dart';
|
import 'package:revanced_manager/services/toast.dart';
|
||||||
import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
|
import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
|
||||||
@@ -16,11 +17,12 @@ class PatchItem extends StatefulWidget {
|
|||||||
required this.packageVersion,
|
required this.packageVersion,
|
||||||
required this.supportedPackageVersions,
|
required this.supportedPackageVersions,
|
||||||
required this.isUnsupported,
|
required this.isUnsupported,
|
||||||
required this.isNew,
|
required this.hasUnsupportedPatchOption,
|
||||||
|
required this.options,
|
||||||
required this.isSelected,
|
required this.isSelected,
|
||||||
required this.onChanged,
|
required this.onChanged,
|
||||||
|
required this.navigateToOptions,
|
||||||
required this.isChangeEnabled,
|
required this.isChangeEnabled,
|
||||||
this.child,
|
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
final String name;
|
final String name;
|
||||||
final String simpleName;
|
final String simpleName;
|
||||||
@@ -28,11 +30,12 @@ class PatchItem extends StatefulWidget {
|
|||||||
final String packageVersion;
|
final String packageVersion;
|
||||||
final List<String> supportedPackageVersions;
|
final List<String> supportedPackageVersions;
|
||||||
final bool isUnsupported;
|
final bool isUnsupported;
|
||||||
final bool isNew;
|
final bool hasUnsupportedPatchOption;
|
||||||
|
final List<Option> options;
|
||||||
bool isSelected;
|
bool isSelected;
|
||||||
final Function(bool) onChanged;
|
final Function(bool) onChanged;
|
||||||
|
final void Function(List<Option>) navigateToOptions;
|
||||||
final bool isChangeEnabled;
|
final bool isChangeEnabled;
|
||||||
final Widget? child;
|
|
||||||
final toast = locator<Toast>();
|
final toast = locator<Toast>();
|
||||||
final _managerAPI = locator<ManagerAPI>();
|
final _managerAPI = locator<ManagerAPI>();
|
||||||
|
|
||||||
@@ -45,157 +48,162 @@ class _PatchItemState extends State<PatchItem> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
widget.isSelected = widget.isSelected &&
|
widget.isSelected = widget.isSelected &&
|
||||||
(!widget.isUnsupported ||
|
(!widget.isUnsupported ||
|
||||||
widget._managerAPI.areExperimentalPatchesEnabled());
|
!widget._managerAPI.isVersionCompatibilityCheckEnabled()) &&
|
||||||
|
!widget.hasUnsupportedPatchOption;
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
||||||
child: Opacity(
|
child: Opacity(
|
||||||
opacity: widget.isUnsupported &&
|
opacity: widget.isUnsupported &&
|
||||||
widget._managerAPI.areExperimentalPatchesEnabled() == false
|
widget._managerAPI.isVersionCompatibilityCheckEnabled() == true
|
||||||
? 0.5
|
? 0.5
|
||||||
: 1,
|
: 1,
|
||||||
child: CustomCard(
|
child: CustomCard(
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
top: 12,
|
||||||
|
bottom: 16,
|
||||||
|
left: 8.0,
|
||||||
|
right: widget.options.isNotEmpty ? 4.0 : 8.0,
|
||||||
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
setState(() {
|
if (widget.isUnsupported &&
|
||||||
if (widget.isUnsupported &&
|
widget._managerAPI.isVersionCompatibilityCheckEnabled()) {
|
||||||
!widget._managerAPI.areExperimentalPatchesEnabled()) {
|
widget.isSelected = false;
|
||||||
widget.isSelected = false;
|
widget.toast.showBottom('patchItem.unsupportedPatchVersion');
|
||||||
widget.toast.showBottom('patchItem.unsupportedPatchVersion');
|
} else if (widget.isChangeEnabled) {
|
||||||
} else if (widget.isChangeEnabled) {
|
if (!widget.isSelected) {
|
||||||
widget.isSelected = !widget.isSelected;
|
if (widget.hasUnsupportedPatchOption) {
|
||||||
|
_showUnsupportedRequiredOptionDialog();
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
widget.isSelected = !widget.isSelected;
|
||||||
if (!widget.isUnsupported || widget._managerAPI.areExperimentalPatchesEnabled()) {
|
setState(() {});
|
||||||
|
}
|
||||||
|
if (!widget.isUnsupported ||
|
||||||
|
!widget._managerAPI.isVersionCompatibilityCheckEnabled()) {
|
||||||
widget.onChanged(widget.isSelected);
|
widget.onChanged(widget.isSelected);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Column(
|
child: Row(
|
||||||
children: <Widget>[
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
Row(
|
children: [
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
Transform.scale(
|
||||||
children: <Widget>[
|
scale: 1.2,
|
||||||
Flexible(
|
child: Checkbox(
|
||||||
child: Column(
|
value: widget.isSelected,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
activeColor: Theme.of(context).colorScheme.primary,
|
||||||
children: <Widget>[
|
checkColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||||
Row(
|
side: BorderSide(
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
width: 2.0,
|
||||||
children: <Widget>[
|
color: Theme.of(context).colorScheme.primary,
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
widget.simpleName,
|
|
||||||
maxLines: 2,
|
|
||||||
overflow: TextOverflow.visible,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
Text(
|
|
||||||
widget.description,
|
|
||||||
softWrap: true,
|
|
||||||
overflow: TextOverflow.visible,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
color: Theme.of(context)
|
|
||||||
.colorScheme
|
|
||||||
.onSecondaryContainer,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
Transform.scale(
|
onChanged: (newValue) {
|
||||||
scale: 1.2,
|
if (widget.isUnsupported &&
|
||||||
child: Checkbox(
|
widget._managerAPI
|
||||||
value: widget.isSelected,
|
.isVersionCompatibilityCheckEnabled()) {
|
||||||
activeColor: Theme.of(context).colorScheme.primary,
|
widget.isSelected = false;
|
||||||
checkColor:
|
widget.toast.showBottom(
|
||||||
Theme.of(context).colorScheme.secondaryContainer,
|
'patchItem.unsupportedPatchVersion',
|
||||||
side: BorderSide(
|
);
|
||||||
width: 2.0,
|
} else if (widget.isChangeEnabled) {
|
||||||
color: Theme.of(context).colorScheme.primary,
|
if (!widget.isSelected) {
|
||||||
),
|
if (widget.hasUnsupportedPatchOption) {
|
||||||
onChanged: (newValue) {
|
_showUnsupportedRequiredOptionDialog();
|
||||||
setState(() {
|
return;
|
||||||
if (widget.isUnsupported &&
|
|
||||||
!widget._managerAPI
|
|
||||||
.areExperimentalPatchesEnabled()) {
|
|
||||||
widget.isSelected = false;
|
|
||||||
widget.toast.showBottom(
|
|
||||||
'patchItem.unsupportedPatchVersion',
|
|
||||||
);
|
|
||||||
} else if (widget.isChangeEnabled) {
|
|
||||||
widget.isSelected = newValue!;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (!widget.isUnsupported || widget._managerAPI.areExperimentalPatchesEnabled()) {
|
|
||||||
widget.onChanged(widget.isSelected);
|
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
),
|
widget.isSelected = newValue!;
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
if (!widget.isUnsupported ||
|
||||||
|
!widget._managerAPI
|
||||||
|
.isVersionCompatibilityCheckEnabled()) {
|
||||||
|
widget.onChanged(widget.isSelected);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 10),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
widget.simpleName,
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.visible,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
widget.description,
|
||||||
|
softWrap: true,
|
||||||
|
overflow: TextOverflow.visible,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onSecondaryContainer,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (widget.description.isNotEmpty)
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.topLeft,
|
||||||
|
child: Wrap(
|
||||||
|
spacing: 8,
|
||||||
|
runSpacing: 4,
|
||||||
|
children: [
|
||||||
|
if (widget.isUnsupported &&
|
||||||
|
!widget._managerAPI
|
||||||
|
.isVersionCompatibilityCheckEnabled())
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 8),
|
||||||
|
child: TextButton.icon(
|
||||||
|
label: I18nText('warning'),
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.warning_amber_outlined,
|
||||||
|
size: 20.0,
|
||||||
|
),
|
||||||
|
onPressed: () =>
|
||||||
|
_showUnsupportedWarningDialog(),
|
||||||
|
style: ButtonStyle(
|
||||||
|
shape: MaterialStateProperty.all(
|
||||||
|
RoundedRectangleBorder(
|
||||||
|
borderRadius:
|
||||||
|
BorderRadius.circular(8),
|
||||||
|
side: BorderSide(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.secondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
backgroundColor:
|
||||||
|
MaterialStateProperty.all(
|
||||||
|
Colors.transparent,
|
||||||
|
),
|
||||||
|
foregroundColor:
|
||||||
|
MaterialStateProperty.all(
|
||||||
|
Theme.of(context).colorScheme.secondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
Row(
|
if (widget.options.isNotEmpty)
|
||||||
children: [
|
IconButton(
|
||||||
if (widget.isUnsupported &&
|
icon: const Icon(Icons.settings_outlined),
|
||||||
widget._managerAPI.areExperimentalPatchesEnabled())
|
onPressed: () => widget.navigateToOptions(widget.options),
|
||||||
Padding(
|
),
|
||||||
padding: const EdgeInsets.only(top: 8, right: 8),
|
|
||||||
child: TextButton.icon(
|
|
||||||
label: I18nText('warning'),
|
|
||||||
icon: const Icon(Icons.warning, size: 20.0),
|
|
||||||
onPressed: () => _showUnsupportedWarningDialog(),
|
|
||||||
style: ButtonStyle(
|
|
||||||
shape: MaterialStateProperty.all(
|
|
||||||
RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
side: BorderSide(
|
|
||||||
color: Theme.of(context).colorScheme.secondary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
backgroundColor: MaterialStateProperty.all(
|
|
||||||
Colors.transparent,
|
|
||||||
),
|
|
||||||
foregroundColor: MaterialStateProperty.all(
|
|
||||||
Theme.of(context).colorScheme.secondary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (widget.isNew)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 8),
|
|
||||||
child: TextButton.icon(
|
|
||||||
label: I18nText('new'),
|
|
||||||
icon: const Icon(Icons.star, size: 20.0),
|
|
||||||
onPressed: () => _showNewPatchDialog(),
|
|
||||||
style: ButtonStyle(
|
|
||||||
shape: MaterialStateProperty.all(
|
|
||||||
RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
side: BorderSide(
|
|
||||||
color: Theme.of(context).colorScheme.secondary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
backgroundColor: MaterialStateProperty.all(
|
|
||||||
Colors.transparent,
|
|
||||||
),
|
|
||||||
foregroundColor: MaterialStateProperty.all(
|
|
||||||
Theme.of(context).colorScheme.secondary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
widget.child ?? const SizedBox(),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -214,7 +222,7 @@ class _PatchItemState extends State<PatchItem> {
|
|||||||
translationParams: {
|
translationParams: {
|
||||||
'packageVersion': widget.packageVersion,
|
'packageVersion': widget.packageVersion,
|
||||||
'supportedVersions':
|
'supportedVersions':
|
||||||
'\u2022 ${widget.supportedPackageVersions.reversed.join('\n\u2022 ')}',
|
'• ${widget.supportedPackageVersions.reversed.join('\n• ')}',
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
@@ -227,14 +235,14 @@ class _PatchItemState extends State<PatchItem> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _showNewPatchDialog() {
|
Future<void> _showUnsupportedRequiredOptionDialog() {
|
||||||
return showDialog(
|
return showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
title: I18nText('patchItem.newPatch'),
|
title: I18nText('notice'),
|
||||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||||
content: I18nText(
|
content: I18nText(
|
||||||
'patchItem.newPatchDialogText',
|
'patchItem.unsupportedRequiredOption',
|
||||||
),
|
),
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
CustomMaterialButton(
|
CustomMaterialButton(
|
||||||
|
|||||||
@@ -1,73 +1,393 @@
|
|||||||
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_i18n/flutter_i18n.dart';
|
import 'package:flutter_i18n/flutter_i18n.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:revanced_manager/models/patch.dart';
|
||||||
|
import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
|
||||||
|
|
||||||
class OptionsTextField extends StatelessWidget {
|
class BooleanPatchOption extends StatelessWidget {
|
||||||
const OptionsTextField({Key? key, required this.hint}) : super(key: key);
|
const BooleanPatchOption({
|
||||||
final String hint;
|
super.key,
|
||||||
|
required this.patchOption,
|
||||||
|
required this.removeOption,
|
||||||
|
required this.onChanged,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Option patchOption;
|
||||||
|
final void Function(Option option) removeOption;
|
||||||
|
final void Function(dynamic value, Option option) onChanged;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final size = MediaQuery.sizeOf(context);
|
final ValueNotifier patchOptionValue = ValueNotifier(patchOption.value);
|
||||||
final sHeight = size.height;
|
return PatchOption(
|
||||||
final sWidth = size.width;
|
widget: Align(
|
||||||
return Container(
|
alignment: Alignment.bottomLeft,
|
||||||
margin: const EdgeInsets.only(top: 12, bottom: 6),
|
child: ValueListenableBuilder(
|
||||||
padding: EdgeInsets.zero,
|
valueListenable: patchOptionValue,
|
||||||
child: TextField(
|
builder: (context, value, child) {
|
||||||
decoration: InputDecoration(
|
return Switch(
|
||||||
constraints: BoxConstraints(
|
value: value ?? false,
|
||||||
maxHeight: sHeight * 0.05,
|
onChanged: (bool value) {
|
||||||
maxWidth: sWidth * 1,
|
patchOptionValue.value = value;
|
||||||
|
onChanged(value, patchOption);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
patchOption: patchOption,
|
||||||
|
removeOption: (Option option) {
|
||||||
|
removeOption(option);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class IntAndStringPatchOption extends StatelessWidget {
|
||||||
|
const IntAndStringPatchOption({
|
||||||
|
super.key,
|
||||||
|
required this.patchOption,
|
||||||
|
required this.removeOption,
|
||||||
|
required this.onChanged,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Option patchOption;
|
||||||
|
final void Function(Option option) removeOption;
|
||||||
|
final void Function(dynamic value, Option option) onChanged;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final ValueNotifier patchOptionValue = ValueNotifier(patchOption.value);
|
||||||
|
return PatchOption(
|
||||||
|
widget: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
TextFieldForPatchOption(
|
||||||
|
value: patchOption.value,
|
||||||
|
optionType: patchOption.optionClassType,
|
||||||
|
onChanged: (value) {
|
||||||
|
patchOptionValue.value = value;
|
||||||
|
onChanged(value, patchOption);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
border: const OutlineInputBorder(),
|
ValueListenableBuilder(
|
||||||
labelText: hint,
|
valueListenable: patchOptionValue,
|
||||||
|
builder: (context, value, child) {
|
||||||
|
if (patchOption.required && value == null) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
I18nText(
|
||||||
|
'patchOptionsView.requiredOption',
|
||||||
|
child: Text(
|
||||||
|
'',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.error,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return const SizedBox();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
patchOption: patchOption,
|
||||||
|
removeOption: (Option option) {
|
||||||
|
removeOption(option);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class IntStringLongListPatchOption extends StatelessWidget {
|
||||||
|
const IntStringLongListPatchOption({
|
||||||
|
super.key,
|
||||||
|
required this.patchOption,
|
||||||
|
required this.removeOption,
|
||||||
|
required this.onChanged,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Option patchOption;
|
||||||
|
final void Function(Option option) removeOption;
|
||||||
|
final void Function(dynamic value, Option option) onChanged;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final String type = patchOption.optionClassType;
|
||||||
|
final List<dynamic> values = patchOption.value ?? [];
|
||||||
|
final ValueNotifier patchOptionValue = ValueNotifier(values);
|
||||||
|
return PatchOption(
|
||||||
|
widget: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
ValueListenableBuilder(
|
||||||
|
valueListenable: patchOptionValue,
|
||||||
|
builder: (context, value, child) {
|
||||||
|
return ListView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
itemCount: value.length,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
itemBuilder: (BuildContext context, int index) {
|
||||||
|
final e = values[index];
|
||||||
|
return TextFieldForPatchOption(
|
||||||
|
value: e.toString(),
|
||||||
|
optionType: type,
|
||||||
|
onChanged: (newValue) {
|
||||||
|
values[index] = type == 'StringListPatchOption'
|
||||||
|
? newValue
|
||||||
|
: type == 'IntListPatchOption'
|
||||||
|
? int.parse(newValue)
|
||||||
|
: num.parse(newValue);
|
||||||
|
onChanged(values, patchOption);
|
||||||
|
},
|
||||||
|
removeValue: (value) {
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
patchOption: patchOption,
|
||||||
|
removeOption: (Option option) {
|
||||||
|
removeOption(option);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class UnsupportedPatchOption extends StatelessWidget {
|
||||||
|
const UnsupportedPatchOption({super.key, required this.patchOption});
|
||||||
|
final Option patchOption;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return PatchOption(
|
||||||
|
widget: Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
|
child: I18nText(
|
||||||
|
'patchOptionsView.unsupportedOption',
|
||||||
|
child: const Text(
|
||||||
|
'',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
patchOption: patchOption,
|
||||||
|
removeOption: (_) {},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PatchOption extends StatelessWidget {
|
||||||
|
const PatchOption({
|
||||||
|
super.key,
|
||||||
|
required this.widget,
|
||||||
|
required this.patchOption,
|
||||||
|
required this.removeOption,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Widget widget;
|
||||||
|
final Option patchOption;
|
||||||
|
final void Function(Option option) removeOption;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: CustomCard(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
patchOption.title,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
patchOption.description,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onSecondaryContainer,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (!patchOption.required)
|
||||||
|
IconButton(
|
||||||
|
onPressed: () => removeOption(patchOption),
|
||||||
|
icon: const Icon(Icons.delete),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
widget,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class OptionsFilePicker extends StatelessWidget {
|
class TextFieldForPatchOption extends StatefulWidget {
|
||||||
const OptionsFilePicker({Key? key, required this.optionName})
|
const TextFieldForPatchOption({
|
||||||
: super(key: key);
|
super.key,
|
||||||
final String optionName;
|
required this.value,
|
||||||
|
this.removeValue,
|
||||||
|
required this.onChanged,
|
||||||
|
required this.optionType,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String? value;
|
||||||
|
final String optionType;
|
||||||
|
final void Function(dynamic value)? removeValue;
|
||||||
|
final void Function(dynamic value) onChanged;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<TextFieldForPatchOption> createState() =>
|
||||||
|
_TextFieldForPatchOptionState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TextFieldForPatchOptionState extends State<TextFieldForPatchOption> {
|
||||||
|
final TextEditingController controller = TextEditingController();
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Padding(
|
final bool isStringOption = widget.optionType.contains('String');
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
final bool isListOption = widget.optionType.contains('List');
|
||||||
child: Row(
|
controller.text = widget.value ?? '';
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
return TextFormField(
|
||||||
children: <Widget>[
|
inputFormatters: [
|
||||||
I18nText(
|
if (widget.optionType.contains('Int'))
|
||||||
optionName,
|
FilteringTextInputFormatter.allow(RegExp(r'[0-9]')),
|
||||||
child: Text(
|
if (widget.optionType.contains('Long'))
|
||||||
'',
|
FilteringTextInputFormatter.allow(RegExp(r'^[0-9]*\.?[0-9]*')),
|
||||||
style: GoogleFonts.inter(
|
],
|
||||||
fontSize: 16,
|
controller: controller,
|
||||||
fontWeight: FontWeight.w500,
|
keyboardType: isStringOption ? TextInputType.text : TextInputType.number,
|
||||||
),
|
decoration: InputDecoration(
|
||||||
),
|
suffixIcon: PopupMenuButton(
|
||||||
|
tooltip: FlutterI18n.translate(
|
||||||
|
context,
|
||||||
|
'patchOptionsView.tooltip',
|
||||||
),
|
),
|
||||||
ElevatedButton(
|
itemBuilder: (BuildContext context) {
|
||||||
style: ButtonStyle(
|
return [
|
||||||
backgroundColor: MaterialStateProperty.all(
|
if (isListOption)
|
||||||
Theme.of(context).colorScheme.primary,
|
PopupMenuItem(
|
||||||
),
|
value: 'remove',
|
||||||
),
|
child: I18nText('remove'),
|
||||||
onPressed: () {
|
),
|
||||||
// pick files
|
if (isStringOption && !isListOption) ...[
|
||||||
},
|
PopupMenuItem(
|
||||||
child: Text(
|
value: 'patchOptionsView.selectFilePath',
|
||||||
'Select File',
|
child: I18nText('patchOptionsView.selectFilePath'),
|
||||||
style: TextStyle(
|
),
|
||||||
color: Theme.of(context).textTheme.bodyLarge?.color,
|
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);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,14 @@
|
|||||||
// ignore_for_file: prefer_const_constructors
|
// ignore_for_file: prefer_const_constructors
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
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_api_url.dart';
|
||||||
import 'package:revanced_manager/ui/views/settings/settingsFragment/settings_manage_sources.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_enable_patches_selection.dart';
|
|
||||||
import 'package:revanced_manager/ui/widgets/settingsView/settings_auto_update_patches.dart';
|
import 'package:revanced_manager/ui/widgets/settingsView/settings_auto_update_patches.dart';
|
||||||
import 'package:revanced_manager/ui/widgets/settingsView/settings_experimental_patches.dart';
|
import 'package:revanced_manager/ui/widgets/settingsView/settings_enable_patches_selection.dart';
|
||||||
import 'package:revanced_manager/ui/widgets/settingsView/settings_experimental_universal_patches.dart';
|
|
||||||
import 'package:revanced_manager/ui/widgets/settingsView/settings_section.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 {
|
class SAdvancedSection extends StatelessWidget {
|
||||||
const SAdvancedSection({super.key});
|
const SAdvancedSection({super.key});
|
||||||
@@ -21,85 +17,14 @@ class SAdvancedSection extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SettingsSection(
|
return SettingsSection(
|
||||||
title: 'settingsView.advancedSectionTitle',
|
title: 'settingsView.advancedSectionTitle',
|
||||||
children: <Widget>[
|
children: const <Widget>[
|
||||||
SManageApiUrlUI(),
|
|
||||||
SManageSourcesUI(),
|
|
||||||
// SManageKeystorePasswordUI(),
|
|
||||||
SAutoUpdatePatches(),
|
SAutoUpdatePatches(),
|
||||||
SEnablePatchesSelection(),
|
SEnablePatchesSelection(),
|
||||||
SExperimentalUniversalPatches(),
|
SVersionCompatibilityCheck(),
|
||||||
SExperimentalPatches(),
|
SUniversalPatches(),
|
||||||
ListTile(
|
SManageSourcesUI(),
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
|
SManageApiUrlUI(),
|
||||||
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(),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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(),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
68
lib/ui/widgets/settingsView/settings_debug_section.dart
Normal file
68
lib/ui/widgets/settingsView/settings_debug_section.dart
Normal 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),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -45,6 +45,46 @@ class SExportSection extends StatelessWidget {
|
|||||||
subtitle: I18nText('settingsView.importPatchesHint'),
|
subtitle: I18nText('settingsView.importPatchesHint'),
|
||||||
onTap: () => _settingsViewModel.importPatches(context),
|
onTap: () => _settingsViewModel.importPatches(context),
|
||||||
),
|
),
|
||||||
|
ListTile(
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||||
|
title: I18nText(
|
||||||
|
'settingsView.resetStoredPatchesLabel',
|
||||||
|
child: const Text(
|
||||||
|
'',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
subtitle: I18nText('settingsView.resetStoredPatchesHint'),
|
||||||
|
onTap: () => _showResetDialog(
|
||||||
|
context,
|
||||||
|
'settingsView.resetStoredPatchesDialogTitle',
|
||||||
|
'settingsView.resetStoredPatchesDialogText',
|
||||||
|
_settingsViewModel.resetSelectedPatches,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||||
|
title: I18nText(
|
||||||
|
'settingsView.resetStoredOptionsLabel',
|
||||||
|
child: const Text(
|
||||||
|
'',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
subtitle: I18nText('settingsView.resetStoredOptionsHint'),
|
||||||
|
onTap: () => _showResetDialog(
|
||||||
|
context,
|
||||||
|
'settingsView.resetStoredOptionsDialogTitle',
|
||||||
|
'settingsView.resetStoredOptionsDialogText',
|
||||||
|
_settingsViewModel.resetAllOptions,
|
||||||
|
),
|
||||||
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||||
title: I18nText(
|
title: I18nText(
|
||||||
@@ -84,7 +124,7 @@ class SExportSection extends StatelessWidget {
|
|||||||
ListTile(
|
ListTile(
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||||
title: I18nText(
|
title: I18nText(
|
||||||
'settingsView.resetStoredPatchesLabel',
|
'settingsView.regenerateKeystoreLabel',
|
||||||
child: const Text(
|
child: const Text(
|
||||||
'',
|
'',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
@@ -93,22 +133,26 @@ class SExportSection extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
subtitle: I18nText('settingsView.resetStoredPatchesHint'),
|
subtitle: I18nText('settingsView.regenerateKeystoreHint'),
|
||||||
onTap: () => _showResetStoredPatchesDialog(context),
|
onTap: () => _showDeleteKeystoreDialog(context),
|
||||||
),
|
),
|
||||||
|
// SManageKeystorePasswordUI(),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _showResetStoredPatchesDialog(context) {
|
Future<void> _showResetDialog(
|
||||||
|
context,
|
||||||
|
dialogTitle,
|
||||||
|
dialogText,
|
||||||
|
dialogAction,
|
||||||
|
) {
|
||||||
return showDialog(
|
return showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
title: I18nText('settingsView.resetStoredPatchesDialogTitle'),
|
title: I18nText(dialogTitle),
|
||||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||||
content: I18nText(
|
content: I18nText(dialogText),
|
||||||
'settingsView.resetStoredPatchesDialogText',
|
|
||||||
),
|
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
CustomMaterialButton(
|
CustomMaterialButton(
|
||||||
isFilled: false,
|
isFilled: false,
|
||||||
@@ -119,7 +163,32 @@ class SExportSection extends StatelessWidget {
|
|||||||
label: I18nText('yesButton'),
|
label: I18nText('yesButton'),
|
||||||
onPressed: () => {
|
onPressed: () => {
|
||||||
Navigator.of(context).pop(),
|
Navigator.of(context).pop(),
|
||||||
_settingsViewModel.resetSelectedPatches(),
|
dialogAction(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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(),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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/patches_selector/patches_selector_viewmodel.dart';
|
||||||
import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart';
|
import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart';
|
||||||
|
|
||||||
class SExperimentalUniversalPatches extends StatefulWidget {
|
class SUniversalPatches extends StatefulWidget {
|
||||||
const SExperimentalUniversalPatches({super.key});
|
const SUniversalPatches({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<SExperimentalUniversalPatches> createState() =>
|
State<SUniversalPatches> createState() =>
|
||||||
_SExperimentalUniversalPatchesState();
|
_SUniversalPatchesState();
|
||||||
}
|
}
|
||||||
|
|
||||||
final _settingsViewModel = SettingsViewModel();
|
final _settingsViewModel = SettingsViewModel();
|
||||||
final _patchesSelectorViewModel = PatchesSelectorViewModel();
|
final _patchesSelectorViewModel = PatchesSelectorViewModel();
|
||||||
final _patcherViewModel = PatcherViewModel();
|
final _patcherViewModel = PatcherViewModel();
|
||||||
|
|
||||||
class _SExperimentalUniversalPatchesState
|
class _SUniversalPatchesState
|
||||||
extends State<SExperimentalUniversalPatches> {
|
extends State<SUniversalPatches> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SwitchListTile(
|
return SwitchListTile(
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||||
title: I18nText(
|
title: I18nText(
|
||||||
'settingsView.experimentalUniversalPatchesLabel',
|
'settingsView.universalPatchesLabel',
|
||||||
child: const Text(
|
child: const Text(
|
||||||
'',
|
'',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
@@ -32,7 +32,7 @@ class _SExperimentalUniversalPatchesState
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
subtitle: I18nText('settingsView.experimentalUniversalPatchesHint'),
|
subtitle: I18nText('settingsView.universalPatchesHint'),
|
||||||
value: _settingsViewModel.areUniversalPatchesEnabled(),
|
value: _settingsViewModel.areUniversalPatchesEnabled(),
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -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/ui/views/settings/settings_viewmodel.dart';
|
||||||
import 'package:revanced_manager/utils/check_for_supported_patch.dart';
|
import 'package:revanced_manager/utils/check_for_supported_patch.dart';
|
||||||
|
|
||||||
class SExperimentalPatches extends StatefulWidget {
|
class SVersionCompatibilityCheck extends StatefulWidget {
|
||||||
const SExperimentalPatches({super.key});
|
const SVersionCompatibilityCheck({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<SExperimentalPatches> createState() => _SExperimentalPatchesState();
|
State<SVersionCompatibilityCheck> createState() => _SVersionCompatibilityCheckState();
|
||||||
}
|
}
|
||||||
|
|
||||||
final _settingsViewModel = SettingsViewModel();
|
final _settingsViewModel = SettingsViewModel();
|
||||||
final _patchesSelectorViewModel = PatchesSelectorViewModel();
|
final _patchesSelectorViewModel = PatchesSelectorViewModel();
|
||||||
final _patcherViewModel = PatcherViewModel();
|
final _patcherViewModel = PatcherViewModel();
|
||||||
|
|
||||||
class _SExperimentalPatchesState extends State<SExperimentalPatches> {
|
class _SVersionCompatibilityCheckState extends State<SVersionCompatibilityCheck> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SwitchListTile(
|
return SwitchListTile(
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||||
title: I18nText(
|
title: I18nText(
|
||||||
'settingsView.experimentalPatchesLabel',
|
'settingsView.versionCompatibilityCheckLabel',
|
||||||
child: const Text(
|
child: const Text(
|
||||||
'',
|
'',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
@@ -31,11 +31,11 @@ class _SExperimentalPatchesState extends State<SExperimentalPatches> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
subtitle: I18nText('settingsView.experimentalPatchesHint'),
|
subtitle: I18nText('settingsView.versionCompatibilityCheckHint'),
|
||||||
value: _settingsViewModel.areExperimentalPatchesEnabled(),
|
value: _settingsViewModel.isVersionCompatibilityCheckEnabled(),
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_settingsViewModel.useExperimentalPatches(value);
|
_settingsViewModel.useVersionCompatibilityCheck(value);
|
||||||
});
|
});
|
||||||
if (!value) {
|
if (!value) {
|
||||||
_patcherViewModel.selectedPatches
|
_patcherViewModel.selectedPatches
|
||||||
@@ -4,6 +4,7 @@ import 'package:flutter_i18n/flutter_i18n.dart';
|
|||||||
import 'package:font_awesome_flutter/font_awesome_flutter.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/settingsView/social_media_item.dart';
|
||||||
import 'package:revanced_manager/ui/widgets/shared/custom_card.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 {
|
class SocialMediaWidget extends StatelessWidget {
|
||||||
const SocialMediaWidget({
|
const SocialMediaWidget({
|
||||||
@@ -42,6 +43,12 @@ class SocialMediaWidget extends StatelessWidget {
|
|||||||
child: const CustomCard(
|
child: const CustomCard(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
|
SocialMediaItem(
|
||||||
|
icon: Icon(CustomIcon.revancedIcon),
|
||||||
|
title: Text('Website'),
|
||||||
|
subtitle: Text('revanced.app'),
|
||||||
|
url: 'https://revanced.app',
|
||||||
|
),
|
||||||
SocialMediaItem(
|
SocialMediaItem(
|
||||||
icon: FaIcon(FontAwesomeIcons.github),
|
icon: FaIcon(FontAwesomeIcons.github),
|
||||||
title: Text('GitHub'),
|
title: Text('GitHub'),
|
||||||
@@ -75,8 +82,8 @@ class SocialMediaWidget extends StatelessWidget {
|
|||||||
SocialMediaItem(
|
SocialMediaItem(
|
||||||
icon: FaIcon(FontAwesomeIcons.youtube),
|
icon: FaIcon(FontAwesomeIcons.youtube),
|
||||||
title: Text('YouTube'),
|
title: Text('YouTube'),
|
||||||
subtitle: Text('youtube.com/revanced'),
|
subtitle: Text('youtube.com/@revanced'),
|
||||||
url: 'https://youtube.com/revanced',
|
url: 'https://youtube.com/@revanced',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:expandable/expandable.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_i18n/flutter_i18n.dart';
|
import 'package:flutter_i18n/flutter_i18n.dart';
|
||||||
import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
|
import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
|
||||||
@@ -13,151 +12,84 @@ class ApplicationItem extends StatefulWidget {
|
|||||||
required this.icon,
|
required this.icon,
|
||||||
required this.name,
|
required this.name,
|
||||||
required this.patchDate,
|
required this.patchDate,
|
||||||
required this.changelog,
|
|
||||||
required this.isUpdatableApp,
|
|
||||||
required this.onPressed,
|
required this.onPressed,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
final Uint8List icon;
|
final Uint8List icon;
|
||||||
final String name;
|
final String name;
|
||||||
final DateTime patchDate;
|
final DateTime patchDate;
|
||||||
final List<String> changelog;
|
|
||||||
final bool isUpdatableApp;
|
|
||||||
final Function() onPressed;
|
final Function() onPressed;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ApplicationItem> createState() => _ApplicationItemState();
|
State<ApplicationItem> createState() => _ApplicationItemState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ApplicationItemState extends State<ApplicationItem>
|
class _ApplicationItemState extends State<ApplicationItem> {
|
||||||
with TickerProviderStateMixin {
|
|
||||||
late AnimationController _animationController;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_animationController = AnimationController(
|
|
||||||
vsync: this,
|
|
||||||
duration: const Duration(milliseconds: 300),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_animationController.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final ExpandableController expController = ExpandableController();
|
|
||||||
return Container(
|
return Container(
|
||||||
margin: const EdgeInsets.only(bottom: 16.0),
|
margin: const EdgeInsets.only(bottom: 16.0),
|
||||||
child: CustomCard(
|
child: CustomCard(
|
||||||
onTap: () {
|
child: Row(
|
||||||
expController.toggle();
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
_animationController.isCompleted
|
children: [
|
||||||
? _animationController.reverse()
|
Flexible(
|
||||||
: _animationController.forward();
|
child: Row(
|
||||||
},
|
|
||||||
child: ExpandablePanel(
|
|
||||||
controller: expController,
|
|
||||||
theme: const ExpandableThemeData(
|
|
||||||
inkWellBorderRadius: BorderRadius.all(Radius.circular(16)),
|
|
||||||
tapBodyToCollapse: false,
|
|
||||||
tapBodyToExpand: false,
|
|
||||||
tapHeaderToExpand: false,
|
|
||||||
hasIcon: false,
|
|
||||||
animationDuration: Duration(milliseconds: 450),
|
|
||||||
),
|
|
||||||
header: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Flexible(
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
SizedBox(
|
|
||||||
width: 40,
|
|
||||||
child: Image.memory(widget.icon, height: 40, width: 40),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 19),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: <Widget>[
|
|
||||||
Text(
|
|
||||||
widget.name,
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
format(widget.patchDate),
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
children: [
|
children: [
|
||||||
RotationTransition(
|
SizedBox(
|
||||||
turns: Tween(begin: 0.0, end: 0.50)
|
width: 40,
|
||||||
.animate(_animationController),
|
child: Image.memory(widget.icon, height: 40, width: 40),
|
||||||
child: const Padding(
|
|
||||||
padding: EdgeInsets.all(8.0),
|
|
||||||
child: Icon(Icons.arrow_drop_down),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 19),
|
||||||
Column(
|
Expanded(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
CustomMaterialButton(
|
Text(
|
||||||
label: widget.isUpdatableApp
|
widget.name,
|
||||||
? I18nText('applicationItem.patchButton')
|
maxLines: 1,
|
||||||
: I18nText('applicationItem.infoButton'),
|
overflow: TextOverflow.ellipsis,
|
||||||
onPressed: widget.onPressed,
|
style: const TextStyle(
|
||||||
),
|
fontSize: 16,
|
||||||
],
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
format(widget.patchDate),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
|
||||||
),
|
|
||||||
collapsed: const SizedBox(),
|
|
||||||
expanded: Padding(
|
|
||||||
padding: const EdgeInsets.only(
|
|
||||||
top: 16.0,
|
|
||||||
left: 4.0,
|
|
||||||
right: 4.0,
|
|
||||||
bottom: 4.0,
|
|
||||||
),
|
),
|
||||||
child: Column(
|
Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
children: [
|
||||||
children: <Widget>[
|
const SizedBox(width: 8),
|
||||||
I18nText(
|
Column(
|
||||||
'applicationItem.changelogLabel',
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
child: const Text(
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
'',
|
children: <Widget>[
|
||||||
style: TextStyle(fontWeight: FontWeight.w700),
|
CustomMaterialButton(
|
||||||
),
|
label: I18nText('applicationItem.infoButton'),
|
||||||
|
onPressed: widget.onPressed,
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
|
||||||
Text('\u2022 ${widget.changelog.join('\n\u2022 ')}'),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
9
lib/ui/widgets/shared/custom_icon.dart
Normal file
9
lib/ui/widgets/shared/custom_icon.dart
Normal 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);
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:revanced_manager/app/app.locator.dart';
|
import 'package:revanced_manager/app/app.locator.dart';
|
||||||
import 'package:revanced_manager/models/patch.dart';
|
import 'package:revanced_manager/models/patch.dart';
|
||||||
import 'package:revanced_manager/models/patched_application.dart';
|
import 'package:revanced_manager/models/patched_application.dart';
|
||||||
|
import 'package:revanced_manager/services/manager_api.dart';
|
||||||
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
|
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
|
||||||
|
|
||||||
bool isPatchSupported(Patch patch) {
|
bool isPatchSupported(Patch patch) {
|
||||||
@@ -12,3 +13,49 @@ bool isPatchSupported(Patch patch) {
|
|||||||
(pack.versions.isEmpty || pack.versions.contains(app.version)),
|
(pack.versions.isEmpty || pack.versions.contains(app.version)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool hasUnsupportedRequiredOption(List<Option> options, Patch patch) {
|
||||||
|
final List<String> requiredOptionsType = [];
|
||||||
|
final List<String> supportedOptionsType = [
|
||||||
|
'StringPatchOption',
|
||||||
|
'BooleanPatchOption',
|
||||||
|
'IntPatchOption',
|
||||||
|
'StringListPatchOption',
|
||||||
|
'IntListPatchOption',
|
||||||
|
'LongListPatchOption',
|
||||||
|
];
|
||||||
|
for (final Option option in options) {
|
||||||
|
if (option.required &&
|
||||||
|
option.value == null &&
|
||||||
|
locator<ManagerAPI>()
|
||||||
|
.getPatchOption(
|
||||||
|
locator<PatcherViewModel>().selectedApp!.packageName,
|
||||||
|
patch.name,
|
||||||
|
option.key,
|
||||||
|
) == null) {
|
||||||
|
requiredOptionsType.add(option.optionClassType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (final String optionType in requiredOptionsType) {
|
||||||
|
if (!supportedOptionsType.contains(optionType)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Option> getNullRequiredOptions(List<Patch> patches, String packageName) {
|
||||||
|
final List<Option> requiredNullOptions = [];
|
||||||
|
for (final patch in patches) {
|
||||||
|
for (final patchOption in patch.options) {
|
||||||
|
if (!patch.excluded &&
|
||||||
|
patchOption.required &&
|
||||||
|
patchOption.value == null &&
|
||||||
|
locator<ManagerAPI>()
|
||||||
|
.getPatchOption(packageName, patch.name, patchOption.key) == null) {
|
||||||
|
requiredNullOptions.add(patchOption);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return requiredNullOptions;
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ homepage: https://github.com/revanced/revanced-manager
|
|||||||
|
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
|
|
||||||
version: 1.9.2+100900200
|
version: 1.13.1+101300100
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.0.0 <4.0.0'
|
sdk: '>=3.0.0 <4.0.0'
|
||||||
@@ -75,6 +75,8 @@ dependencies:
|
|||||||
flutter_markdown: ^0.6.14
|
flutter_markdown: ^0.6.14
|
||||||
dio_cache_interceptor: ^3.4.0
|
dio_cache_interceptor: ^3.4.0
|
||||||
install_plugin: ^2.1.0
|
install_plugin: ^2.1.0
|
||||||
|
screenshot_callback: ^3.0.1
|
||||||
|
synchronized: ^3.1.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
json_serializable: ^6.6.1
|
json_serializable: ^6.6.1
|
||||||
@@ -89,5 +91,9 @@ dev_dependencies:
|
|||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
|
fonts:
|
||||||
|
- family: CustomIcon
|
||||||
|
fonts:
|
||||||
|
- asset: fonts/custom-icons.ttf
|
||||||
assets:
|
assets:
|
||||||
- assets/i18n/
|
- assets/i18n/
|
||||||
|
|||||||
2
settings.gradle
Normal file
2
settings.gradle
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
include ':build.gradle'
|
||||||
|
project(':build.gradle').projectDir = new File(rootDir, 'android/build.gradle')
|
||||||
Reference in New Issue
Block a user