mirror of
https://github.com/ReVanced/revanced-manager.git
synced 2025-05-18 23:16:50 +08:00
Compare commits
108 Commits
Author | SHA1 | Date | |
---|---|---|---|
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
|
||||
description: Report a very clearly broken issue.
|
||||
description: Create a new bug report.
|
||||
title: 'bug: <title>'
|
||||
labels: [bug]
|
||||
body:
|
||||
@ -8,53 +8,20 @@ body:
|
||||
value: |
|
||||
# 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).
|
||||
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Type
|
||||
options:
|
||||
- Error while running the manager
|
||||
- Error at runtime
|
||||
- Cosmetic
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
Please check for existing issues [here](https://github.com/revanced/revanced-manager/labels/bug) before creating a new one.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Bug description
|
||||
description: 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:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Steps to reproduce
|
||||
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.
|
||||
label: Version of ReVanced Manager and version & name of application you tried to patch
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
@ -64,57 +31,31 @@ body:
|
||||
- Non-root
|
||||
- Root
|
||||
validations:
|
||||
required: true
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Patches selected.
|
||||
description: Patches you selected for the app.
|
||||
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.
|
||||
label: Device logs
|
||||
description: Export logs in ReVanced Manager settings.
|
||||
render: shell
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Installer logs (exported using Installer menu option) [unneeded if the issue is not during patching].
|
||||
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so there is no need for backticks.
|
||||
label: Patcher logs
|
||||
description: Export logs in "Patcher" screen.
|
||||
render: shell
|
||||
validations:
|
||||
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
|
||||
id: acknowledgments
|
||||
attributes:
|
||||
label: Acknowledgments
|
||||
description: Your issue will be closed if you haven't done these steps.
|
||||
label: Acknowledgements
|
||||
description: Your issue will be closed if you don't follow the checklist below!
|
||||
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
|
||||
- label: I have written a short but informative title.
|
||||
- label: I have chosen an appropriate title.
|
||||
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
|
||||
- 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
|
||||
|
40
.github/ISSUE_TEMPLATE/feature-issue.yml
vendored
40
.github/ISSUE_TEMPLATE/feature-issue.yml
vendored
@ -1,52 +1,42 @@
|
||||
name: ⭐ Feature request
|
||||
description: Create a detailed feature request.
|
||||
description: Create a new feature request.
|
||||
title: 'feat: <title>'
|
||||
labels: [feature-request]
|
||||
body:
|
||||
- type: dropdown
|
||||
- type: markdown
|
||||
attributes:
|
||||
label: Type
|
||||
options:
|
||||
- Functionality
|
||||
- Cosmetic
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
value: |
|
||||
# ReVanced Manager feature request
|
||||
|
||||
Please check for existing feature requests [here](https://github.com/revanced/revanced-manager/labels/bug) before creating a new one.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Issue
|
||||
description: What is the current problem. Why does it require a feature request?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Feature
|
||||
description: Describe your feature in detail. How does it solve the issue?
|
||||
label: Feature description
|
||||
description: Describe your feature in detail.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Motivation
|
||||
description: Why should your feature should be considered?
|
||||
description: Explain why the lack of it is a problem.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add additional context here.
|
||||
description: In case there is something else you want to add.
|
||||
validations:
|
||||
required: false
|
||||
- type: checkboxes
|
||||
id: acknowledgements
|
||||
attributes:
|
||||
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:
|
||||
- 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
|
||||
- label: I have written a short but informative title.
|
||||
- label: I have chosen an appropriate title.
|
||||
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
|
||||
- label: The issue is related solely to the ReVanced Manager
|
||||
- label: The issue is solely related to the ReVanced Manager
|
||||
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
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
# Make sure the release step uses its own credentials:
|
||||
# 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:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set env
|
||||
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
||||
- name: Set up JDK 11
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -58,6 +58,7 @@ unlinked.ds
|
||||
unlinked_spec.ds
|
||||
|
||||
# Android related
|
||||
.gradle/
|
||||
**/android/**/gradle-wrapper.jar
|
||||
**/android/.gradle
|
||||
**/android/captures/
|
||||
|
22
README.md
22
README.md
@ -3,29 +3,33 @@
|
||||
The official ReVanced Manager based on Flutter.
|
||||
|
||||
## 🔽 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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
[](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
|
||||
|
||||
1. Setup flutter environment for your [platform](https://docs.flutter.dev/get-started/install)
|
||||
2. Clone the repository locally
|
||||
3. Add your github token in gradle.properties like [this](/docs/4_building.md)
|
||||
3. Add your GitHub token in gradle.properties like [this](/docs/4_building.md)
|
||||
4. Open the project in terminal
|
||||
5. Run `flutter pub get` in terminal
|
||||
6. Then `flutter packages pub run build_runner build --delete-conflicting-outputs` (Must be done on each git pull)
|
||||
7. To build release apk run `flutter build apk`
|
||||
7. To build release APK run `flutter build apk`
|
||||
|
@ -85,10 +85,9 @@ dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
|
||||
// ReVanced
|
||||
implementation "app.revanced:revanced-patcher:11.0.4"
|
||||
implementation "app.revanced:revanced-patcher:17.0.0"
|
||||
|
||||
// Signing & aligning
|
||||
implementation("org.bouncycastle:bcpkix-jdk15on:1.70")
|
||||
implementation("com.android.tools.build:apksig:7.2.2")
|
||||
|
||||
}
|
||||
|
@ -25,8 +25,7 @@
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:largeHeap="true"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:extractNativeLibs="true"
|
||||
android:enableOnBackInvokedCallback="true">
|
||||
android:extractNativeLibs="true">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
@ -43,6 +42,10 @@
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ExportSettingsActivity"
|
||||
android:exported="true">
|
||||
</activity>
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
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
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.annotation.NonNull
|
||||
import app.revanced.manager.flutter.utils.Aapt
|
||||
import app.revanced.manager.flutter.utils.aligning.ZipAligner
|
||||
import app.revanced.manager.flutter.utils.signing.Signer
|
||||
import app.revanced.manager.flutter.utils.zip.ZipFile
|
||||
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.PatcherOptions
|
||||
import app.revanced.patcher.extensions.PatchExtensions.compatiblePackages
|
||||
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 app.revanced.patcher.patch.PatchResult
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import java.io.File
|
||||
|
||||
private const val PATCHER_CHANNEL = "app.revanced.manager.flutter/patcher"
|
||||
private const val INSTALLER_CHANNEL = "app.revanced.manager.flutter/installer"
|
||||
import java.io.PrintWriter
|
||||
import java.io.StringWriter
|
||||
import java.util.logging.LogRecord
|
||||
import java.util.logging.Logger
|
||||
|
||||
class MainActivity : FlutterActivity() {
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
@ -30,31 +31,42 @@ class MainActivity : FlutterActivity() {
|
||||
private var cancel: Boolean = false
|
||||
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)
|
||||
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 ->
|
||||
when (call.method) {
|
||||
"runPatcher" -> {
|
||||
val patchBundleFilePath = call.argument<String>("patchBundleFilePath")
|
||||
val originalFilePath = call.argument<String>("originalFilePath")
|
||||
val inputFilePath = call.argument<String>("inputFilePath")
|
||||
val patchedFilePath = call.argument<String>("patchedFilePath")
|
||||
val outFilePath = call.argument<String>("outFilePath")
|
||||
val integrationsPath = call.argument<String>("integrationsPath")
|
||||
val selectedPatches = call.argument<List<String>>("selectedPatches")
|
||||
val options = call.argument<Map<String, Map<String, Any>>>("options")
|
||||
val cacheDirPath = call.argument<String>("cacheDirPath")
|
||||
val keyStoreFilePath = call.argument<String>("keyStoreFilePath")
|
||||
val keystorePassword = call.argument<String>("keystorePassword")
|
||||
|
||||
if (patchBundleFilePath != null &&
|
||||
if (
|
||||
originalFilePath != null &&
|
||||
inputFilePath != null &&
|
||||
patchedFilePath != null &&
|
||||
outFilePath != null &&
|
||||
integrationsPath != null &&
|
||||
selectedPatches != null &&
|
||||
options != null &&
|
||||
cacheDirPath != null &&
|
||||
keyStoreFilePath != null &&
|
||||
keystorePassword != null
|
||||
@ -62,20 +74,18 @@ class MainActivity : FlutterActivity() {
|
||||
cancel = false
|
||||
runPatcher(
|
||||
result,
|
||||
patchBundleFilePath,
|
||||
originalFilePath,
|
||||
inputFilePath,
|
||||
patchedFilePath,
|
||||
outFilePath,
|
||||
integrationsPath,
|
||||
selectedPatches,
|
||||
options,
|
||||
cacheDirPath,
|
||||
keyStoreFilePath,
|
||||
keystorePassword
|
||||
)
|
||||
} else {
|
||||
result.notImplemented()
|
||||
}
|
||||
} else result.notImplemented()
|
||||
}
|
||||
|
||||
"stopPatcher" -> {
|
||||
@ -83,6 +93,69 @@ class MainActivity : FlutterActivity() {
|
||||
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()
|
||||
}
|
||||
}
|
||||
@ -90,13 +163,13 @@ class MainActivity : FlutterActivity() {
|
||||
|
||||
private fun runPatcher(
|
||||
result: MethodChannel.Result,
|
||||
patchBundleFilePath: String,
|
||||
originalFilePath: String,
|
||||
inputFilePath: String,
|
||||
patchedFilePath: String,
|
||||
outFilePath: String,
|
||||
integrationsPath: String,
|
||||
selectedPatches: List<String>,
|
||||
options: Map<String, Map<String, Any>>,
|
||||
cacheDirPath: String,
|
||||
keyStoreFilePath: String,
|
||||
keystorePassword: String
|
||||
@ -107,179 +180,146 @@ class MainActivity : FlutterActivity() {
|
||||
val outFile = File(outFilePath)
|
||||
val integrations = File(integrationsPath)
|
||||
val keyStoreFile = File(keyStoreFilePath)
|
||||
val cacheDir = File(cacheDirPath)
|
||||
|
||||
Thread {
|
||||
try {
|
||||
fun updateProgress(progress: Double, header: String, log: String) {
|
||||
handler.post {
|
||||
installerChannel.invokeMethod(
|
||||
"update",
|
||||
mapOf(
|
||||
"progress" to 0.1,
|
||||
"header" to "",
|
||||
"log" to "Copying original APK"
|
||||
"progress" to progress,
|
||||
"header" to header,
|
||||
"log" to log
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if(cancel) {
|
||||
handler.post { stopResult!!.success(null) }
|
||||
fun postStop() = 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
|
||||
}
|
||||
|
||||
originalFile.copyTo(inputFile, true)
|
||||
|
||||
handler.post {
|
||||
installerChannel.invokeMethod(
|
||||
"update",
|
||||
mapOf(
|
||||
"progress" to 0.2,
|
||||
"header" to "Reading APK...",
|
||||
"log" to "Reading input APK"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if(cancel) {
|
||||
handler.post { stopResult!!.success(null) }
|
||||
if (cancel) {
|
||||
postStop()
|
||||
return@Thread
|
||||
}
|
||||
|
||||
val patcher =
|
||||
Patcher(
|
||||
PatcherOptions(
|
||||
inputFile,
|
||||
cacheDirPath,
|
||||
Aapt.binary(applicationContext).absolutePath,
|
||||
cacheDirPath,
|
||||
logger = ManagerLogger()
|
||||
)
|
||||
updateProgress(0.05, "Reading APK...", "Reading APK")
|
||||
|
||||
val patcher = Patcher(
|
||||
PatcherOptions(
|
||||
inputFile,
|
||||
cacheDir,
|
||||
Aapt.binary(applicationContext).absolutePath,
|
||||
cacheDir.path,
|
||||
true // TODO: Add option to disable this
|
||||
)
|
||||
)
|
||||
|
||||
if (cancel) {
|
||||
handler.post { stopResult!!.success(null) }
|
||||
postStop()
|
||||
return@Thread
|
||||
}
|
||||
|
||||
handler.post {
|
||||
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"
|
||||
)
|
||||
)
|
||||
}
|
||||
updateProgress(0.1, "Loading patches...", "Loading patches")
|
||||
|
||||
if(cancel) {
|
||||
handler.post { stopResult!!.success(null) }
|
||||
return@Thread
|
||||
}
|
||||
val patches = patches.filter { patch ->
|
||||
val isCompatible = patch.compatiblePackages?.any {
|
||||
it.name == patcher.context.packageMetadata.packageName
|
||||
} ?: false
|
||||
|
||||
patcher.addIntegrations(listOf(integrations)) {}
|
||||
val compatibleOrUniversal =
|
||||
isCompatible || patch.compatiblePackages.isNullOrEmpty()
|
||||
|
||||
if(cancel) {
|
||||
handler.post { stopResult!!.success(null) }
|
||||
return@Thread
|
||||
}
|
||||
|
||||
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 }
|
||||
compatibleOrUniversal && selectedPatches.any { it == patch.name }
|
||||
}.onEach { patch ->
|
||||
options[patch.name]?.forEach { (key, value) ->
|
||||
patch.options[key] = value
|
||||
}
|
||||
} else {
|
||||
TODO("VERSION.SDK_INT < CUPCAKE")
|
||||
}
|
||||
|
||||
if(cancel) {
|
||||
handler.post { stopResult!!.success(null) }
|
||||
if (cancel) {
|
||||
postStop()
|
||||
return@Thread
|
||||
}
|
||||
|
||||
patcher.addPatches(patches)
|
||||
patcher.executePatches().forEach { (patch, res) ->
|
||||
if (res.isSuccess) {
|
||||
val msg = "Applied $patch"
|
||||
handler.post {
|
||||
installerChannel.invokeMethod(
|
||||
"update",
|
||||
mapOf(
|
||||
"progress" to 0.5,
|
||||
"header" to "",
|
||||
"log" to msg
|
||||
)
|
||||
)
|
||||
updateProgress(0.15, "Executing...", "")
|
||||
|
||||
// Update the progress bar every time a patch is executed from 0.15 to 0.7
|
||||
val totalPatchesCount = patches.size
|
||||
val progressStep = 0.55 / totalPatchesCount
|
||||
var progress = 0.15
|
||||
|
||||
patcher.apply {
|
||||
acceptIntegrations(listOf(integrations))
|
||||
acceptPatches(patches)
|
||||
|
||||
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 {
|
||||
installerChannel.invokeMethod(
|
||||
"update",
|
||||
mapOf(
|
||||
"progress" to 0.7,
|
||||
"header" to "Repacking APK...",
|
||||
"log" to ""
|
||||
)
|
||||
)
|
||||
}
|
||||
if(cancel) {
|
||||
handler.post { stopResult!!.success(null) }
|
||||
if (cancel) {
|
||||
postStop()
|
||||
patcher.close()
|
||||
return@Thread
|
||||
}
|
||||
val res = patcher.save()
|
||||
|
||||
updateProgress(0.8, "Building...", "")
|
||||
|
||||
val res = patcher.get()
|
||||
patcher.close()
|
||||
|
||||
ZipFile(patchedFile).use { file ->
|
||||
res.dexFiles.forEach {
|
||||
if (cancel) {
|
||||
handler.post { stopResult!!.success(null) }
|
||||
postStop()
|
||||
return@Thread
|
||||
}
|
||||
file.addEntryCompressData(
|
||||
@ -298,90 +338,35 @@ class MainActivity : FlutterActivity() {
|
||||
ZipAligner::getEntryAlignment
|
||||
)
|
||||
}
|
||||
|
||||
if (cancel) {
|
||||
handler.post { stopResult!!.success(null) }
|
||||
postStop()
|
||||
return@Thread
|
||||
}
|
||||
handler.post {
|
||||
installerChannel.invokeMethod(
|
||||
"update",
|
||||
mapOf(
|
||||
"progress" to 0.9,
|
||||
"header" to "Signing APK...",
|
||||
"log" to ""
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
updateProgress(0.9, "Signing...", "Signing APK")
|
||||
|
||||
try {
|
||||
Signer("ReVanced", keystorePassword).signApk(
|
||||
patchedFile,
|
||||
outFile,
|
||||
keyStoreFile
|
||||
)
|
||||
Signer("ReVanced", keystorePassword)
|
||||
.signApk(patchedFile, outFile, keyStoreFile)
|
||||
} catch (e: Exception) {
|
||||
//log to console
|
||||
print("Error signing APK: ${e.message}")
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
handler.post {
|
||||
installerChannel.invokeMethod(
|
||||
"update",
|
||||
mapOf(
|
||||
"progress" to 1.0,
|
||||
"header" to "Finished!",
|
||||
"log" to "Finished!"
|
||||
)
|
||||
)
|
||||
}
|
||||
updateProgress(1.0, "Patched", "Patched")
|
||||
} catch (ex: Throwable) {
|
||||
val stack = ex.stackTraceToString()
|
||||
handler.post {
|
||||
installerChannel.invokeMethod(
|
||||
"update",
|
||||
mapOf(
|
||||
"progress" to -100.0,
|
||||
"header" to "Aborted...",
|
||||
"log" to "An error occurred! Aborted\nError:\n$stack"
|
||||
)
|
||||
if (!cancel) {
|
||||
val stack = ex.stackTraceToString()
|
||||
updateProgress(
|
||||
-100.0,
|
||||
"Failed",
|
||||
"An error occurred:\n$stack"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
handler.post { result.success(null) }
|
||||
}.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 {
|
||||
ext.kotlin_version = '1.7.10'
|
||||
ext.kotlin_version = '1.9.0'
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
@ -16,11 +16,7 @@ allprojects {
|
||||
google()
|
||||
mavenCentral()
|
||||
maven {
|
||||
url = uri("https://maven.pkg.github.com/revanced/revanced-patcher")
|
||||
credentials {
|
||||
username = (project.findProperty("gpr.user") ?: System.getenv("GITHUB_ACTOR")) as String
|
||||
password = (project.findProperty("gpr.key") ?: System.getenv("GITHUB_TOKEN")) as String
|
||||
}
|
||||
url 'https://jitpack.io'
|
||||
}
|
||||
mavenLocal()
|
||||
}
|
||||
|
@ -1,7 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionSha256Sum=a01b6587e15fe7ed120a0ee299c25982a1eee045abd6a9dd5e216b2f628ef9ac
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.2-bin.zip
|
||||
networkTimeout=10000
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip
|
||||
|
@ -10,9 +10,11 @@
|
||||
"yesButton": "Yes",
|
||||
"noButton": "No",
|
||||
"warning": "Warning",
|
||||
"options": "Options",
|
||||
"notice": "Notice",
|
||||
"noShowAgain": "Don't show this again",
|
||||
"new": "New",
|
||||
"add": "Add",
|
||||
"remove": "Remove",
|
||||
"navigationView": {
|
||||
"dashboardTab": "Dashboard",
|
||||
"patcherTab": "Patcher",
|
||||
@ -23,13 +25,13 @@
|
||||
"widgetTitle": "Dashboard",
|
||||
|
||||
"updatesSubtitle": "Updates",
|
||||
"patchedSubtitle": "Patched applications",
|
||||
"patchedSubtitle": "Patched apps",
|
||||
|
||||
"noUpdates": "No updates available",
|
||||
|
||||
"WIP": "Work in progress...",
|
||||
|
||||
"noInstallations": "No patched applications installed",
|
||||
"noInstallations": "No patched apps installed",
|
||||
"installUpdate": "Continue to install the update?",
|
||||
|
||||
"updateDialogTitle": "Update Manager",
|
||||
@ -56,9 +58,7 @@
|
||||
"updatesDisabled": "Updating a patched app is currently disabled. Repatch the app again."
|
||||
},
|
||||
"applicationItem": {
|
||||
"patchButton": "Patch",
|
||||
"infoButton": "Info",
|
||||
"changelogLabel": "Changelog"
|
||||
"infoButton": "Info"
|
||||
},
|
||||
"latestCommitCard": {
|
||||
"loadingLabel": "Loading...",
|
||||
@ -71,10 +71,10 @@
|
||||
"widgetTitle": "Patcher",
|
||||
"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?",
|
||||
"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": {
|
||||
"widgetTitle": "Select an application",
|
||||
@ -117,6 +117,8 @@
|
||||
"viewTitle": "Select patches",
|
||||
"searchBarHint": "Search patches",
|
||||
"universalPatches": "Universal patches",
|
||||
"newPatches": "New patches",
|
||||
"patches": "Patches",
|
||||
|
||||
"doneButton": "Done",
|
||||
|
||||
@ -129,15 +131,29 @@
|
||||
"loadPatchesSelection": "Load patches selection",
|
||||
"noSavedPatches": "No saved patches for the selected app.\nPress Done to save current selection.",
|
||||
"noPatchesFound": "No patches found for the selected app",
|
||||
"setRequiredOption": "Some patches require options to be set:\n\n{patches}\n\nPlease set them before continuing.",
|
||||
|
||||
"selectAllPatchesWarningContent": "You are about to select all patches, that includes non-suggested patches and can cause unwanted behavior."
|
||||
},
|
||||
"patchOptionsView": {
|
||||
"viewTitle": "Patch options",
|
||||
"saveOptions": "Save",
|
||||
|
||||
"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": {
|
||||
"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.",
|
||||
|
||||
"newPatchDialogText": "This is a new patch that has been added since the last time you have patched this app.",
|
||||
"newPatch": "New patch",
|
||||
"unsupportedRequiredOption": "This patch contains a required option that is not supported by this app",
|
||||
|
||||
"patchesChangeWarningDialogText": "It is recommended to use the default selection of patches because changing it may cause unexpected issues.\n\nIf you know what you are doing, you can enable \"Enable changing selection\" in the settings.",
|
||||
"patchesChangeWarningDialogButton": "Use default selection"
|
||||
@ -148,9 +164,8 @@
|
||||
"installTypeDescription": "Select the installation type to proceed with.",
|
||||
|
||||
"installButton": "Install",
|
||||
"installRootType": "Root",
|
||||
"installNonRootType": "Non-root",
|
||||
"installRecommendedType": "Recommended",
|
||||
"installRootType": "Mount",
|
||||
"installNonRootType": "Normal",
|
||||
|
||||
"pressBackAgain": "Press back again to cancel",
|
||||
"openButton": "Open",
|
||||
@ -162,10 +177,9 @@
|
||||
"exportApkButtonTooltip": "Export patched APK",
|
||||
"exportLogButtonTooltip": "Export log",
|
||||
|
||||
"installErrorDialogTitle": "Error",
|
||||
"installErrorDialogText1": "Root install is not possible with the current patches selection.\nRepatch your app or choose non-root install.",
|
||||
"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.",
|
||||
"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?",
|
||||
"copiedToClipboard": "Copied log to clipboard",
|
||||
|
||||
"noExit": "Installer is still running, cannot exit..."
|
||||
},
|
||||
"settingsView": {
|
||||
@ -178,8 +192,10 @@
|
||||
"exportSectionTitle": "Import & export",
|
||||
"logsSectionTitle": "Logs",
|
||||
|
||||
"darkThemeLabel": "Dark mode",
|
||||
"darkThemeHint": "Welcome to the dark side",
|
||||
"themeModeLabel": "App theme",
|
||||
"systemThemeLabel": "System",
|
||||
"lightThemeLabel": "Light",
|
||||
"darkThemeLabel": "Dark",
|
||||
|
||||
"dynamicThemeLabel": "Material You",
|
||||
"dynamicThemeHint": "Enjoy an experience closer to your device",
|
||||
@ -242,10 +258,17 @@
|
||||
"resetStoredPatchesLabel": "Reset patches",
|
||||
"resetStoredPatchesHint": "Reset the stored patches selection",
|
||||
|
||||
"resetStoredOptionsLabel": "Reset options",
|
||||
"resetStoredOptionsHint": "Reset all patch options",
|
||||
|
||||
"resetStoredPatchesDialogTitle": "Reset patches selection?",
|
||||
"resetStoredPatchesDialogText": "Resetting patches selection will remove all selected patches.",
|
||||
"resetStoredPatches": "Patches selection has been reset",
|
||||
|
||||
"resetStoredOptionsDialogTitle": "Reset options?",
|
||||
"resetStoredOptionsDialogText": "Resetting options will remove all saved options.",
|
||||
"resetStoredOptions": "Options have been reset",
|
||||
|
||||
"deleteLogsLabel": "Delete logs",
|
||||
"deleteLogsHint": "Delete collected manager logs",
|
||||
"deletedLogs": "Logs deleted",
|
||||
@ -283,7 +306,6 @@
|
||||
"rootDialogText": "App was installed with superuser permissions, but currently ReVanced Manager has no permissions.\nPlease grant superuser permissions first.",
|
||||
|
||||
"packageNameLabel": "Package name",
|
||||
"originalPackageNameLabel": "Original package name",
|
||||
"installTypeLabel": "Installation type",
|
||||
"rootTypeLabel": "Root",
|
||||
"nonRootTypeLabel": "Non-root",
|
||||
|
11
crowdin.yml
11
crowdin.yml
@ -1,9 +1,4 @@
|
||||
project_id_env: CROWDIN_PROJECT_ID
|
||||
api_token_env: CROWDIN_PERSONAL_TOKEN
|
||||
|
||||
commit_message: 'chore(i18n): sync translations'
|
||||
|
||||
preserve_hierarchy: true
|
||||
preserve_hierarchy: 1
|
||||
files:
|
||||
- source: assets/i18n/en_US.json
|
||||
translation: assets/i18n/%locale_with_underscore%.json
|
||||
- source: /assets/i18n/en_US.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
|
||||
2. Tap on the **Select an app** card
|
||||
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.
|
||||
[^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.
|
||||
|
||||
|
@ -6,29 +6,38 @@ ReVanced Manager has settings that can be configured to your liking.
|
||||
|
||||
- ### 🔗 API URL
|
||||
|
||||
Specify the URL of the API to use. This is used to fetch ReVanced Patches and update ReVanced Manager.
|
||||
API to use to fetch updates and ReVanced Patches from.
|
||||
|
||||
- ### 🧬 Sources
|
||||
|
||||
Override the API and change the source of ReVanced Patches.
|
||||
Override the API and download ReVanced Patches from a different source.
|
||||
|
||||
- ### 🧪 Experimental ReVanced Patches support
|
||||
|
||||
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.
|
||||
Disable checking for the version of the app when applying ReVanced Patches.
|
||||
|
||||
> [!WARNING]
|
||||
> This may cause issues if the ReVanced Patches are not compatible with the app version.
|
||||
|
||||
- ### 🧑🔬 Experimental universal support
|
||||
|
||||
This will show or hide ReVanced Patches, which are not meant for any app in particular but rather for all apps but may not work on all apps.
|
||||
This will show or hide ReVanced Patches, which are not meant for any app in particular but apply to all apps
|
||||
|
||||
- ### 🔑 Export, import or delete keystore
|
||||
> [!WARNING]
|
||||
> Because the patches generalize the app, they may not work on all apps.
|
||||
|
||||
Manage the keystore used to sign patched apps.
|
||||
- ### 💾 Imports & Exports
|
||||
|
||||
- ### 📄 Export, import or reset ReVanced Patches selection
|
||||
You can import, export or reset the following settings:
|
||||
|
||||
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.
|
||||
- 🔑 Keystore
|
||||
- 📄 ReVanced Patches selection
|
||||
- ⚙️ Options
|
||||
|
||||
- ### ℹ️ About
|
||||
> [!NOTE]
|
||||
> This is particularly useful if you want to backup or reset your settings.
|
||||
|
||||
- ### ❓ About
|
||||
|
||||
View information about your device and ReVanced Manager. This includes the version of ReVanced Manager and supported architectures of your device.
|
||||
|
||||
|
@ -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.
|
||||
|
||||
- 💥 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.
|
||||
|
||||
@ -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.
|
||||
|
||||
- 🚫 Non-root install is not possible with the current patches selection
|
||||
|
||||
Select the **Default** button when choosing patches.
|
||||
|
||||
- 🚨 Patched app crashes on launch
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
gpr.user = YourUsername
|
||||
|
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
|
@ -9,6 +9,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/navigation/navigation_view.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_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/views/patches_selector/patches_selector_view.dart';
|
||||
@ -23,6 +25,7 @@ import 'package:stacked_services/stacked_services.dart';
|
||||
MaterialRoute(page: PatcherView),
|
||||
MaterialRoute(page: AppSelectorView),
|
||||
MaterialRoute(page: PatchesSelectorView),
|
||||
MaterialRoute(page: PatchOptionsView),
|
||||
MaterialRoute(page: InstallerView),
|
||||
MaterialRoute(page: SettingsView),
|
||||
MaterialRoute(page: ContributorsView),
|
||||
@ -32,6 +35,7 @@ import 'package:stacked_services/stacked_services.dart';
|
||||
LazySingleton(classType: NavigationViewModel),
|
||||
LazySingleton(classType: HomeViewModel),
|
||||
LazySingleton(classType: PatcherViewModel),
|
||||
LazySingleton(classType: PatchOptionsViewModel),
|
||||
LazySingleton(classType: NavigationService),
|
||||
LazySingleton(classType: ManagerAPI),
|
||||
LazySingleton(classType: PatcherAPI),
|
||||
|
@ -1,5 +1,4 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:revanced_manager/utils/string.dart';
|
||||
|
||||
part 'patch.g.dart';
|
||||
|
||||
@ -9,26 +8,21 @@ class Patch {
|
||||
required this.name,
|
||||
required this.description,
|
||||
required this.excluded,
|
||||
required this.dependencies,
|
||||
required this.compatiblePackages,
|
||||
required this.options,
|
||||
});
|
||||
|
||||
factory Patch.fromJson(Map<String, dynamic> json) => _$PatchFromJson(json);
|
||||
final String name;
|
||||
final String description;
|
||||
final String? description;
|
||||
final bool excluded;
|
||||
final List<String> dependencies;
|
||||
final List<Package> compatiblePackages;
|
||||
final List<Option> options;
|
||||
|
||||
Map<String, dynamic> toJson() => _$PatchToJson(this);
|
||||
|
||||
String getSimpleName() {
|
||||
return name
|
||||
.replaceAll('-', ' ')
|
||||
.split('-')
|
||||
.join(' ')
|
||||
.toTitleCase()
|
||||
.replaceFirst('Microg', 'MicroG');
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
@ -41,8 +35,32 @@ class Package {
|
||||
|
||||
factory Package.fromJson(Map<String, dynamic> json) =>
|
||||
_$PackageFromJson(json);
|
||||
|
||||
final String name;
|
||||
final List<String> versions;
|
||||
|
||||
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({
|
||||
required this.name,
|
||||
required this.packageName,
|
||||
required this.originalPackageName,
|
||||
required this.version,
|
||||
required this.apkFilePath,
|
||||
required this.icon,
|
||||
required this.patchDate,
|
||||
this.isRooted = false,
|
||||
this.isFromStorage = false,
|
||||
this.hasUpdates = false,
|
||||
this.appliedPatches = const [],
|
||||
this.changelog = const [],
|
||||
});
|
||||
|
||||
factory PatchedApplication.fromJson(Map<String, dynamic> json) =>
|
||||
_$PatchedApplicationFromJson(json);
|
||||
String name;
|
||||
String packageName;
|
||||
String originalPackageName;
|
||||
String version;
|
||||
final String apkFilePath;
|
||||
@JsonKey(
|
||||
@ -36,9 +32,7 @@ class PatchedApplication {
|
||||
DateTime patchDate;
|
||||
bool isRooted;
|
||||
bool isFromStorage;
|
||||
bool hasUpdates;
|
||||
List<String> appliedPatches;
|
||||
List<String> changelog;
|
||||
|
||||
Map<String, dynamic> toJson() => _$PatchedApplicationToJson(this);
|
||||
|
||||
|
@ -1,4 +1,3 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
@ -7,7 +6,6 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:revanced_manager/app/app.locator.dart';
|
||||
import 'package:revanced_manager/models/patch.dart';
|
||||
import 'package:revanced_manager/services/manager_api.dart';
|
||||
|
||||
@lazySingleton
|
||||
@ -21,17 +19,6 @@ class GithubAPI {
|
||||
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 {
|
||||
try {
|
||||
_dio = Dio(
|
||||
@ -142,38 +129,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(
|
||||
String extension,
|
||||
String repoName,
|
||||
@ -222,10 +177,8 @@ class GithubAPI {
|
||||
final String downloadUrl = asset['browser_download_url'];
|
||||
if (extension == '.apk') {
|
||||
_managerAPI.setIntegrationsDownloadURL(downloadUrl);
|
||||
} else if (extension == '.json') {
|
||||
_managerAPI.setPatchesDownloadURL(downloadUrl, false);
|
||||
} else {
|
||||
_managerAPI.setPatchesDownloadURL(downloadUrl, true);
|
||||
_managerAPI.setPatchesDownloadURL(downloadUrl);
|
||||
}
|
||||
return await DefaultCacheManager().getSingleFile(
|
||||
downloadUrl,
|
||||
@ -239,30 +192,4 @@ class GithubAPI {
|
||||
}
|
||||
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/patched_application.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/root_api.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 cliRepo = 'revanced-cli';
|
||||
late SharedPreferences _prefs;
|
||||
List<Patch> patches = [];
|
||||
List<Option> modifiedOptions = [];
|
||||
List<Option> options = [];
|
||||
Patch? selectedPatch;
|
||||
BuildContext? ctx;
|
||||
bool isRooted = false;
|
||||
String storedPatchesFile = '/selected-patches.json';
|
||||
String keystoreFile =
|
||||
@ -40,12 +46,14 @@ class ManagerAPI {
|
||||
String defaultManagerRepo = 'revanced/revanced-manager';
|
||||
String? patchesVersion = '';
|
||||
String? integrationsVersion = '';
|
||||
|
||||
bool isDefaultPatchesRepo() {
|
||||
return getPatchesRepo() == 'revanced/revanced-patches';
|
||||
return getPatchesRepo().toLowerCase() == 'revanced/revanced-patches';
|
||||
}
|
||||
|
||||
bool isDefaultIntegrationsRepo() {
|
||||
return getIntegrationsRepo() == 'revanced/revanced-integrations';
|
||||
return getIntegrationsRepo().toLowerCase() ==
|
||||
'revanced/revanced-integrations';
|
||||
}
|
||||
|
||||
Future<void> initialize() async {
|
||||
@ -79,12 +87,12 @@ class ManagerAPI {
|
||||
await _prefs.setString('repoUrl', url);
|
||||
}
|
||||
|
||||
String getPatchesDownloadURL(bool bundle) {
|
||||
return _prefs.getString('patchesDownloadURL-$bundle') ?? '';
|
||||
String getPatchesDownloadURL() {
|
||||
return _prefs.getString('patchesDownloadURL') ?? '';
|
||||
}
|
||||
|
||||
Future<void> setPatchesDownloadURL(String value, bool bundle) async {
|
||||
await _prefs.setString('patchesDownloadURL-$bundle', value);
|
||||
Future<void> setPatchesDownloadURL(String value) async {
|
||||
await _prefs.setString('patchesDownloadURL', value);
|
||||
}
|
||||
|
||||
String getPatchesRepo() {
|
||||
@ -111,17 +119,6 @@ class ManagerAPI {
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@ -189,6 +186,29 @@ class ManagerAPI {
|
||||
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() {
|
||||
return _prefs.getString('integrationsRepo') ?? defaultIntegrationsRepo;
|
||||
}
|
||||
@ -208,12 +228,12 @@ class ManagerAPI {
|
||||
await _prefs.setBool('useDynamicTheme', value);
|
||||
}
|
||||
|
||||
bool getUseDarkTheme() {
|
||||
return _prefs.getBool('useDarkTheme') ?? false;
|
||||
int getThemeMode() {
|
||||
return _prefs.getInt('themeMode') ?? 2;
|
||||
}
|
||||
|
||||
Future<void> setUseDarkTheme(bool value) async {
|
||||
await _prefs.setBool('useDarkTheme', value);
|
||||
Future<void> setThemeMode(int value) async {
|
||||
await _prefs.setInt('themeMode', value);
|
||||
}
|
||||
|
||||
bool areUniversalPatchesEnabled() {
|
||||
@ -311,28 +331,45 @@ class ManagerAPI {
|
||||
}
|
||||
|
||||
Future<List<Patch>> getPatches() async {
|
||||
try {
|
||||
final String repoName = getPatchesRepo();
|
||||
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 [];
|
||||
if (patches.isNotEmpty) {
|
||||
return patches;
|
||||
}
|
||||
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 {
|
||||
try {
|
||||
final String repoName = getPatchesRepo();
|
||||
final String currentVersion = await getCurrentPatchesVersion();
|
||||
final String url = getPatchesDownloadURL(true);
|
||||
final String url = getPatchesDownloadURL();
|
||||
return await _githubAPI.getPatchesReleaseFile(
|
||||
'.jar',
|
||||
repoName,
|
||||
@ -458,8 +495,7 @@ class ManagerAPI {
|
||||
|
||||
Future<void> setCurrentPatchesVersion(String version) async {
|
||||
await _prefs.setString('patchesVersion', version);
|
||||
await setPatchesDownloadURL('', false);
|
||||
await setPatchesDownloadURL('', true);
|
||||
await setPatchesDownloadURL('');
|
||||
await downloadPatches();
|
||||
}
|
||||
|
||||
@ -495,62 +531,33 @@ class ManagerAPI {
|
||||
return toRemove;
|
||||
}
|
||||
|
||||
Future<List<PatchedApplication>> getUnsavedApps(
|
||||
List<PatchedApplication> patchedApps,
|
||||
) async {
|
||||
final List<PatchedApplication> unsavedApps = [];
|
||||
Future<List<PatchedApplication>> getMountedApps() async {
|
||||
final List<PatchedApplication> mountedApps = [];
|
||||
final bool hasRootPermissions = await _rootAPI.hasRootPermissions();
|
||||
if (hasRootPermissions) {
|
||||
final List<String> installedApps = await _rootAPI.getInstalledApps();
|
||||
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(
|
||||
app.packageName,
|
||||
packageName,
|
||||
true,
|
||||
) as ApplicationWithIcon?;
|
||||
if (application != null) {
|
||||
unsavedApps.add(
|
||||
mountedApps.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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return unsavedApps;
|
||||
|
||||
return mountedApps;
|
||||
}
|
||||
|
||||
Future<void> showPatchesChangeWarningDialog(BuildContext context) {
|
||||
@ -612,34 +619,20 @@ class ManagerAPI {
|
||||
|
||||
Future<void> reAssessSavedApps() async {
|
||||
final List<PatchedApplication> patchedApps = getPatchedApps();
|
||||
final List<PatchedApplication> unsavedApps =
|
||||
await getUnsavedApps(patchedApps);
|
||||
patchedApps.addAll(unsavedApps);
|
||||
|
||||
// Remove apps that are not installed anymore.
|
||||
final List<PatchedApplication> toRemove =
|
||||
await getAppsToRemove(patchedApps);
|
||||
patchedApps.removeWhere((a) => toRemove.contains(a));
|
||||
for (final PatchedApplication app in patchedApps) {
|
||||
app.hasUpdates =
|
||||
await hasAppUpdates(app.originalPackageName, app.patchDate);
|
||||
app.changelog =
|
||||
await getAppChangelog(app.originalPackageName, app.patchDate);
|
||||
if (!app.hasUpdates) {
|
||||
final String? currentInstalledVersion =
|
||||
(await DeviceApps.getApp(app.packageName))?.versionName;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Determine all apps that are installed by mounting.
|
||||
final List<PatchedApplication> mountedApps = await getMountedApps();
|
||||
mountedApps.removeWhere(
|
||||
(app) => patchedApps
|
||||
.any((patchedApp) => patchedApp.packageName == app.packageName),
|
||||
);
|
||||
patchedApps.addAll(mountedApps);
|
||||
|
||||
await setPatchedApps(patchedApps);
|
||||
}
|
||||
|
||||
@ -656,37 +649,6 @@ class ManagerAPI {
|
||||
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 {
|
||||
Application? app;
|
||||
if (patchedApp.isFromStorage) {
|
||||
@ -752,8 +714,18 @@ class ManagerAPI {
|
||||
return jsonDecode(string);
|
||||
}
|
||||
|
||||
void resetAllOptions() {
|
||||
_prefs.getKeys().where((key) => key.startsWith('patchOption-')).forEach(
|
||||
(key) {
|
||||
_prefs.remove(key);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> resetLastSelectedPatches() async {
|
||||
final File selectedPatchesFile = File(storedPatchesFile);
|
||||
selectedPatchesFile.deleteSync();
|
||||
if (selectedPatchesFile.existsSync()) {
|
||||
selectedPatchesFile.deleteSync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,19 +18,20 @@ import 'package:share_extend/share_extend.dart';
|
||||
@lazySingleton
|
||||
class PatcherAPI {
|
||||
static const patcherChannel =
|
||||
MethodChannel('app.revanced.manager.flutter/patcher');
|
||||
MethodChannel('app.revanced.manager.flutter/patcher');
|
||||
final ManagerAPI _managerAPI = locator<ManagerAPI>();
|
||||
final RootAPI _rootAPI = RootAPI();
|
||||
late Directory _dataDir;
|
||||
late Directory _tmpDir;
|
||||
late File _keyStoreFile;
|
||||
List<Patch> _patches = [];
|
||||
List<Patch> _universalPatches = [];
|
||||
List<String> _compatiblePackages = [];
|
||||
Map filteredPatches = <String, List<Patch>>{};
|
||||
File? _outFile;
|
||||
File? outFile;
|
||||
|
||||
Future<void> initialize() async {
|
||||
await _loadPatches();
|
||||
await _managerAPI.downloadPatches();
|
||||
await loadPatches();
|
||||
await _managerAPI.downloadIntegrations();
|
||||
final Directory appCache = await getTemporaryDirectory();
|
||||
_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 {
|
||||
if (_patches.isEmpty) {
|
||||
_patches = await _managerAPI.getPatches();
|
||||
@ -56,63 +73,59 @@ class PatcherAPI {
|
||||
}
|
||||
_patches = List.empty();
|
||||
}
|
||||
|
||||
_compatiblePackages = getCompatiblePackages();
|
||||
_universalPatches = getUniversalPatches();
|
||||
}
|
||||
|
||||
Future<List<ApplicationWithIcon>> getFilteredInstalledApps(
|
||||
bool showUniversalPatches,
|
||||
) async {
|
||||
bool showUniversalPatches,) async {
|
||||
final List<ApplicationWithIcon> filteredApps = [];
|
||||
final bool allAppsIncluded =
|
||||
_patches.any((patch) => patch.compatiblePackages.isEmpty) &&
|
||||
showUniversalPatches;
|
||||
_universalPatches.isNotEmpty && showUniversalPatches;
|
||||
if (allAppsIncluded) {
|
||||
final allPackages = await DeviceApps.getInstalledApplications(
|
||||
final appList = await DeviceApps.getInstalledApplications(
|
||||
includeAppIcons: true,
|
||||
onlyAppsWithLaunchIntent: true,
|
||||
);
|
||||
for (final pkg in allPackages) {
|
||||
if (!filteredApps.any((app) => app.packageName == pkg.packageName)) {
|
||||
final appInfo = await DeviceApps.getApp(
|
||||
pkg.packageName,
|
||||
true,
|
||||
) as ApplicationWithIcon?;
|
||||
if (appInfo != null) {
|
||||
filteredApps.add(appInfo);
|
||||
}
|
||||
}
|
||||
|
||||
for (final app in appList) {
|
||||
filteredApps.add(app as ApplicationWithIcon);
|
||||
}
|
||||
}
|
||||
for (final Patch patch in _patches) {
|
||||
for (final Package package in patch.compatiblePackages) {
|
||||
try {
|
||||
if (!filteredApps.any((app) => app.packageName == package.name)) {
|
||||
final ApplicationWithIcon? app = await DeviceApps.getApp(
|
||||
package.name,
|
||||
true,
|
||||
) as ApplicationWithIcon?;
|
||||
if (app != null) {
|
||||
filteredApps.add(app);
|
||||
}
|
||||
}
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
for (final packageName in _compatiblePackages) {
|
||||
try {
|
||||
if (!filteredApps.any((app) => app.packageName == packageName)) {
|
||||
final ApplicationWithIcon? app = await DeviceApps.getApp(
|
||||
packageName,
|
||||
true,
|
||||
) as ApplicationWithIcon?;
|
||||
if (app != null) {
|
||||
filteredApps.add(app);
|
||||
}
|
||||
}
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
return filteredApps;
|
||||
}
|
||||
|
||||
List<Patch> getFilteredPatches(String packageName) {
|
||||
if (!_compatiblePackages.contains(packageName)) {
|
||||
return _universalPatches;
|
||||
}
|
||||
|
||||
final List<Patch> patches = _patches
|
||||
.where(
|
||||
(patch) =>
|
||||
patch.compatiblePackages.isEmpty ||
|
||||
!patch.name.contains('settings') &&
|
||||
patch.compatiblePackages
|
||||
.any((pack) => pack.name == packageName),
|
||||
)
|
||||
patch.compatiblePackages.isEmpty ||
|
||||
!patch.name.contains('settings') &&
|
||||
patch.compatiblePackages
|
||||
.any((pack) => pack.name == packageName),
|
||||
)
|
||||
.toList();
|
||||
if (!_managerAPI.areUniversalPatchesEnabled()) {
|
||||
filteredPatches[packageName] = patches
|
||||
@ -124,77 +137,52 @@ class PatcherAPI {
|
||||
return filteredPatches[packageName];
|
||||
}
|
||||
|
||||
Future<List<Patch>> getAppliedPatches(
|
||||
List<String> appliedPatches,
|
||||
) async {
|
||||
Future<List<Patch>> getAppliedPatches(List<String> appliedPatches,) async {
|
||||
return _patches
|
||||
.where((patch) => appliedPatches.contains(patch.name))
|
||||
.toList();
|
||||
}
|
||||
|
||||
Future<bool> needsResourcePatching(
|
||||
List<Patch> selectedPatches,
|
||||
) async {
|
||||
return selectedPatches.any(
|
||||
(patch) => patch.dependencies.any(
|
||||
(dep) => dep.contains('resource-'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> needsSettingsPatch(List<Patch> selectedPatches) async {
|
||||
return selectedPatches.any(
|
||||
(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);
|
||||
Future<void> runPatcher(String packageName,
|
||||
String apkFilePath,
|
||||
List<Patch> selectedPatches,) async {
|
||||
final File? integrationsFile = await _managerAPI.downloadIntegrations();
|
||||
final Map<String, Map<String, dynamic>> options = {};
|
||||
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);
|
||||
if (patchOption != null) {
|
||||
patchOptions[patchOption.key] = patchOption.value;
|
||||
}
|
||||
}
|
||||
options[patch.name] = patchOptions;
|
||||
}
|
||||
}
|
||||
final File? patchBundleFile = await _managerAPI.downloadPatches();
|
||||
final File? integrationsFile = await _managerAPI.downloadIntegrations();
|
||||
if (patchBundleFile != null) {
|
||||
|
||||
if (integrationsFile != null) {
|
||||
_dataDir.createSync();
|
||||
_tmpDir.createSync();
|
||||
final Directory workDir = _tmpDir.createTempSync('tmp-');
|
||||
final File inputFile = File('${workDir.path}/base.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');
|
||||
cacheDir.createSync();
|
||||
final String originalFilePath = apkFilePath;
|
||||
|
||||
try {
|
||||
await patcherChannel.invokeMethod(
|
||||
'runPatcher',
|
||||
{
|
||||
'patchBundleFilePath': patchBundleFile.path,
|
||||
'originalFilePath': originalFilePath,
|
||||
'inputFilePath': inputFile.path,
|
||||
'patchedFilePath': patchedFile.path,
|
||||
'outFilePath': _outFile!.path,
|
||||
'integrationsPath': integrationsFile!.path,
|
||||
'outFilePath': outFile!.path,
|
||||
'integrationsPath': integrationsFile.path,
|
||||
'selectedPatches': selectedPatches.map((p) => p.name).toList(),
|
||||
'options': options,
|
||||
'cacheDirPath': cacheDir.path,
|
||||
'keyStoreFilePath': _keyStoreFile.path,
|
||||
'keystorePassword': _managerAPI.getKeystorePassword(),
|
||||
@ -206,131 +194,131 @@ class PatcherAPI {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> stopPatcher() async {
|
||||
try {
|
||||
await patcherChannel.invokeMethod('stopPatcher');
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
Future<void> stopPatcher() async {
|
||||
try {
|
||||
await patcherChannel.invokeMethod('stopPatcher');
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
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 '';
|
||||
}}
|
||||
|
@ -7,12 +7,15 @@ import 'package:dio_cache_interceptor/dio_cache_interceptor.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:synchronized/synchronized.dart';
|
||||
import 'package:timeago/timeago.dart';
|
||||
|
||||
@lazySingleton
|
||||
class RevancedAPI {
|
||||
late Dio _dio = Dio();
|
||||
|
||||
final Lock getToolsLock = Lock();
|
||||
|
||||
final _cacheOptions = CacheOptions(
|
||||
store: MemCacheStore(),
|
||||
maxStale: const Duration(days: 1),
|
||||
@ -66,21 +69,23 @@ class RevancedAPI {
|
||||
Future<Map<String, dynamic>?> _getLatestRelease(
|
||||
String extension,
|
||||
String repoName,
|
||||
) async {
|
||||
try {
|
||||
final response = await _dio.get('/tools');
|
||||
final List<dynamic> tools = response.data['tools'];
|
||||
return tools.firstWhereOrNull(
|
||||
(t) =>
|
||||
t['repository'] == repoName &&
|
||||
(t['name'] as String).endsWith(extension),
|
||||
);
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
) {
|
||||
return getToolsLock.synchronized(() async {
|
||||
try {
|
||||
final response = await _dio.get('/tools');
|
||||
final List<dynamic> tools = response.data['tools'];
|
||||
return tools.firstWhereOrNull(
|
||||
(t) =>
|
||||
t['repository'] == repoName &&
|
||||
(t['name'] as String).endsWith(extension),
|
||||
);
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<String?> getLatestReleaseVersion(
|
||||
|
@ -1,12 +1,16 @@
|
||||
import 'dart:ui';
|
||||
import 'package:dynamic_color/dynamic_color.dart';
|
||||
import 'package:dynamic_themes/dynamic_themes.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.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/services/manager_api.dart';
|
||||
import 'package:revanced_manager/theme.dart';
|
||||
import 'package:stacked_services/stacked_services.dart';
|
||||
|
||||
class DynamicThemeBuilder extends StatelessWidget {
|
||||
class DynamicThemeBuilder extends StatefulWidget {
|
||||
const DynamicThemeBuilder({
|
||||
Key? key,
|
||||
required this.title,
|
||||
@ -17,6 +21,35 @@ class DynamicThemeBuilder extends StatelessWidget {
|
||||
final Widget home;
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
return DynamicColorBuilder(
|
||||
@ -50,24 +83,32 @@ class DynamicThemeBuilder extends StatelessWidget {
|
||||
return DynamicTheme(
|
||||
themeCollection: ThemeCollection(
|
||||
themes: {
|
||||
0: lightCustomTheme,
|
||||
1: darkCustomTheme,
|
||||
2: lightDynamicTheme,
|
||||
3: darkDynamicTheme,
|
||||
0: brightness == Brightness.light ? lightCustomTheme : darkCustomTheme,
|
||||
1: brightness == Brightness.light ? lightDynamicTheme : darkDynamicTheme,
|
||||
2: lightCustomTheme,
|
||||
3: lightDynamicTheme,
|
||||
4: darkCustomTheme,
|
||||
5: darkDynamicTheme,
|
||||
},
|
||||
fallbackTheme: lightCustomTheme,
|
||||
fallbackTheme: PlatformDispatcher.instance.platformBrightness == Brightness.light ? lightCustomTheme : darkCustomTheme,
|
||||
),
|
||||
builder: (context, theme) => MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
title: title,
|
||||
navigatorKey: StackedService.navigatorKey,
|
||||
onGenerateRoute: StackedRouter().onGenerateRoute,
|
||||
theme: theme,
|
||||
home: home,
|
||||
localizationsDelegates: localizationsDelegates,
|
||||
),
|
||||
debugShowCheckedModeBanner: false,
|
||||
title: widget.title,
|
||||
navigatorKey: StackedService.navigatorKey,
|
||||
onGenerateRoute: StackedRouter().onGenerateRoute,
|
||||
theme: theme,
|
||||
home: widget.home,
|
||||
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(),
|
||||
viewModelBuilder: () => AppSelectorViewModel(),
|
||||
builder: (context, model, child) => Scaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
label: I18nText('appSelectorView.storageButton'),
|
||||
icon: const Icon(Icons.sd_storage),
|
||||
@ -54,7 +53,7 @@ class _AppSelectorViewState extends State<AppSelectorView> {
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
bottom: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(64.0),
|
||||
preferredSize: const Size.fromHeight(66.0),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
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/ui/views/patcher/patcher_viewmodel.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';
|
||||
|
||||
class AppSelectorViewModel extends BaseViewModel {
|
||||
@ -73,13 +74,12 @@ class AppSelectorViewModel extends BaseViewModel {
|
||||
locator<PatcherViewModel>().selectedApp = PatchedApplication(
|
||||
name: application.appName,
|
||||
packageName: application.packageName,
|
||||
originalPackageName: application.packageName,
|
||||
version: application.versionName!,
|
||||
apkFilePath: application.apkFilePath,
|
||||
icon: application.icon,
|
||||
patchDate: DateTime.now(),
|
||||
);
|
||||
locator<PatcherViewModel>().loadLastSelectedPatches();
|
||||
await locator<PatcherViewModel>().loadLastSelectedPatches();
|
||||
}
|
||||
|
||||
Future<void> canSelectInstalled(
|
||||
@ -90,10 +90,18 @@ class AppSelectorViewModel extends BaseViewModel {
|
||||
await DeviceApps.getApp(packageName, true) as ApplicationWithIcon?;
|
||||
if (app != null) {
|
||||
if (await checkSplitApk(packageName) && !isRooted) {
|
||||
return showSelectFromStorageDialog(context);
|
||||
if (context.mounted) {
|
||||
return showSelectFromStorageDialog(context);
|
||||
}
|
||||
} else if (!await checkSplitApk(packageName) || isRooted) {
|
||||
selectApp(app);
|
||||
Navigator.pop(context);
|
||||
await selectApp(app);
|
||||
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(
|
||||
name: application.appName,
|
||||
packageName: application.packageName,
|
||||
originalPackageName: application.packageName,
|
||||
version: application.versionName!,
|
||||
apkFilePath: result.files.single.path!,
|
||||
icon: application.icon,
|
||||
@ -222,7 +229,8 @@ class AppSelectorViewModel extends BaseViewModel {
|
||||
(app) =>
|
||||
query.isEmpty ||
|
||||
query.length < 2 ||
|
||||
app.appName.toLowerCase().contains(query.toLowerCase()),
|
||||
app.appName.toLowerCase().contains(query.toLowerCase()) ||
|
||||
app.packageName.toLowerCase().contains(query.toLowerCase()),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
@ -34,10 +34,8 @@ class HomeViewModel extends BaseViewModel {
|
||||
final RevancedAPI _revancedAPI = locator<RevancedAPI>();
|
||||
final Toast _toast = locator<Toast>();
|
||||
final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
|
||||
DateTime? _lastUpdate;
|
||||
bool showUpdatableApps = false;
|
||||
List<PatchedApplication> patchedInstalledApps = [];
|
||||
List<PatchedApplication> patchedUpdatableApps = [];
|
||||
String? _latestManagerVersion = '';
|
||||
File? downloadedApk;
|
||||
|
||||
@ -82,7 +80,7 @@ class HomeViewModel extends BaseViewModel {
|
||||
_toast.showBottom('homeView.errorDownloadMessage');
|
||||
}
|
||||
}
|
||||
_getPatchedApps();
|
||||
|
||||
_managerAPI.reAssessSavedApps().then((_) => _getPatchedApps());
|
||||
}
|
||||
|
||||
@ -108,10 +106,6 @@ class HomeViewModel extends BaseViewModel {
|
||||
|
||||
void _getPatchedApps() {
|
||||
patchedInstalledApps = _managerAPI.getPatchedApps().toList();
|
||||
patchedUpdatableApps = _managerAPI
|
||||
.getPatchedApps()
|
||||
.where((app) => app.hasUpdates == true)
|
||||
.toList();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@ -469,11 +463,7 @@ class HomeViewModel extends BaseViewModel {
|
||||
}
|
||||
|
||||
Future<void> forceRefresh(BuildContext context) async {
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
if (_lastUpdate == null ||
|
||||
_lastUpdate!.difference(DateTime.now()).inSeconds > 2) {
|
||||
_managerAPI.clearAllData();
|
||||
}
|
||||
_managerAPI.clearAllData();
|
||||
_toast.showBottom('homeView.refreshSuccess');
|
||||
initialize(context);
|
||||
}
|
||||
|
@ -21,11 +21,23 @@ class InstallerView extends StatelessWidget {
|
||||
bottom: false,
|
||||
child: Scaffold(
|
||||
floatingActionButton: Visibility(
|
||||
visible: !model.isPatching,
|
||||
visible: !model.isPatching && !model.hasErrors,
|
||||
child: FloatingActionButton.extended(
|
||||
label: I18nText('installerView.installButton'),
|
||||
icon: const Icon(Icons.file_download_outlined),
|
||||
onPressed: () => model.installTypeDialog(context),
|
||||
label: I18nText(
|
||||
model.isInstalled
|
||||
? '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,
|
||||
),
|
||||
),
|
||||
|
@ -15,6 +15,8 @@ import 'package:revanced_manager/services/root_api.dart';
|
||||
import 'package:revanced_manager/services/toast.dart';
|
||||
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
|
||||
import 'package:revanced_manager/utils/about_info.dart';
|
||||
import 'package:screenshot_callback/screenshot_callback.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
import 'package:wakelock/wakelock.dart';
|
||||
|
||||
@ -29,6 +31,7 @@ class InstallerViewModel extends BaseViewModel {
|
||||
'app.revanced.manager.flutter/installer',
|
||||
);
|
||||
final ScrollController scrollController = ScrollController();
|
||||
final ScreenshotCallback screenshotCallback = ScreenshotCallback();
|
||||
double? progress = 0.0;
|
||||
String logs = '';
|
||||
String headerLogs = '';
|
||||
@ -38,6 +41,7 @@ class InstallerViewModel extends BaseViewModel {
|
||||
bool hasErrors = false;
|
||||
bool isCanceled = false;
|
||||
bool cancel = false;
|
||||
bool showPopupScreenshotWarning = true;
|
||||
|
||||
Future<void> initialize(BuildContext context) async {
|
||||
isRooted = await _rootAPI.isRooted();
|
||||
@ -64,6 +68,12 @@ class InstallerViewModel extends BaseViewModel {
|
||||
} // ignore
|
||||
}
|
||||
}
|
||||
screenshotCallback.addListener(() {
|
||||
if (showPopupScreenshotWarning) {
|
||||
showPopupScreenshotWarning = false;
|
||||
screenshotDetected(context);
|
||||
}
|
||||
});
|
||||
await Wakelock.enable();
|
||||
await handlePlatformChannelMethods();
|
||||
await runPatcher();
|
||||
@ -129,29 +139,30 @@ class InstallerViewModel extends BaseViewModel {
|
||||
}
|
||||
|
||||
Future<void> runPatcher() async {
|
||||
|
||||
try {
|
||||
update(0.0, 'Initializing...', 'Initializing installer');
|
||||
if (_patches.isNotEmpty) {
|
||||
try {
|
||||
update(0.1, '', 'Creating working directory');
|
||||
await _patcherAPI.runPatcher(
|
||||
_app.packageName,
|
||||
_app.apkFilePath,
|
||||
_patches,
|
||||
);
|
||||
} on Exception catch (e) {
|
||||
update(
|
||||
-100.0,
|
||||
'Aborted...',
|
||||
'An error occurred! Aborted\nError:\n$e',
|
||||
);
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
update(-100.0, 'Aborted...', 'No app or patches selected! Aborted');
|
||||
await _patcherAPI.runPatcher(
|
||||
_app.packageName,
|
||||
_app.apkFilePath,
|
||||
_patches,
|
||||
);
|
||||
} on Exception catch (e) {
|
||||
update(
|
||||
-100.0,
|
||||
'Failed...',
|
||||
'Something went wrong:\n$e',
|
||||
);
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
|
||||
// Necessary to reset the state of patches so that they
|
||||
// can be reloaded again.
|
||||
_managerAPI.patches.clear();
|
||||
await _patcherAPI.loadPatches();
|
||||
|
||||
try {
|
||||
if (FlutterBackground.isBackgroundExecutionEnabled) {
|
||||
try {
|
||||
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',
|
||||
'Enabled changing patches: ${_managerAPI.isPatchesChangeEnabled()}',
|
||||
'Enabled universal patches: ${_managerAPI.areUniversalPatchesEnabled()}',
|
||||
'Enabled experimental patches: ${_managerAPI.areExperimentalPatchesEnabled()}',
|
||||
'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 {
|
||||
final ValueNotifier<int> installType = ValueNotifier(0);
|
||||
if (isRooted) {
|
||||
@ -182,52 +259,55 @@ class InstallerViewModel extends BaseViewModel {
|
||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
icon: const Icon(Icons.file_download_outlined),
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 16),
|
||||
content: ValueListenableBuilder(
|
||||
valueListenable: installType,
|
||||
builder: (context, value, child) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 20,
|
||||
vertical: 10,
|
||||
),
|
||||
child: I18nText(
|
||||
'installerView.installTypeDescription',
|
||||
child: Text(
|
||||
'',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
content: SingleChildScrollView(
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: installType,
|
||||
builder: (context, value, child) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 20,
|
||||
vertical: 10,
|
||||
),
|
||||
child: I18nText(
|
||||
'installerView.installTypeDescription',
|
||||
child: Text(
|
||||
'',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
RadioListTile(
|
||||
title: I18nText('installerView.installNonRootType'),
|
||||
subtitle: I18nText('installerView.installRecommendedType'),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
value: 0,
|
||||
groupValue: value,
|
||||
onChanged: (selected) {
|
||||
installType.value = selected!;
|
||||
},
|
||||
),
|
||||
RadioListTile(
|
||||
title: I18nText('installerView.installRootType'),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
value: 1,
|
||||
groupValue: value,
|
||||
onChanged: (selected) {
|
||||
installType.value = selected!;
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
RadioListTile(
|
||||
title: I18nText('installerView.installNonRootType'),
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 16),
|
||||
value: 0,
|
||||
groupValue: value,
|
||||
onChanged: (selected) {
|
||||
installType.value = selected!;
|
||||
},
|
||||
),
|
||||
RadioListTile(
|
||||
title: I18nText('installerView.installRootType'),
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 16),
|
||||
value: 1,
|
||||
groupValue: value,
|
||||
onChanged: (selected) {
|
||||
installType.value = selected!;
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
CustomMaterialButton(
|
||||
@ -255,9 +335,9 @@ class InstallerViewModel extends BaseViewModel {
|
||||
Future<void> stopPatcher() async {
|
||||
try {
|
||||
isCanceled = true;
|
||||
update(0.5, 'Aborting...', 'Canceling patching process');
|
||||
update(0.5, 'Canceling...', 'Canceling patching process');
|
||||
await _patcherAPI.stopPatcher();
|
||||
update(-100.0, 'Aborted...', 'Press back to exit');
|
||||
update(-100.0, 'Canceled...', 'Press back to exit');
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
@ -268,56 +348,33 @@ class InstallerViewModel extends BaseViewModel {
|
||||
Future<void> installResult(BuildContext context, bool installAsRoot) async {
|
||||
try {
|
||||
_app.isRooted = installAsRoot;
|
||||
final bool hasMicroG =
|
||||
_patches.any((p) => p.name.endsWith('MicroG support'));
|
||||
final bool rootMicroG = installAsRoot && hasMicroG;
|
||||
final bool rootFromStorage = installAsRoot && _app.isFromStorage;
|
||||
final bool ytWithoutRootMicroG =
|
||||
!installAsRoot && !hasMicroG && _app.packageName.contains('youtube');
|
||||
if (rootMicroG || rootFromStorage || ytWithoutRootMicroG) {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: I18nText('installerView.installErrorDialogTitle'),
|
||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
content: I18nText(
|
||||
rootMicroG
|
||||
? 'installerView.installErrorDialogText1'
|
||||
: rootFromStorage
|
||||
? 'installerView.installErrorDialogText3'
|
||||
: 'installerView.installErrorDialogText2',
|
||||
),
|
||||
actions: <Widget>[
|
||||
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);
|
||||
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) {
|
||||
_app.isFromStorage = false;
|
||||
_app.patchDate = DateTime.now();
|
||||
_app.appliedPatches = _patches.map((p) => p.name).toList();
|
||||
|
||||
// In case a patch changed the app name or package name,
|
||||
// update the app info.
|
||||
final app =
|
||||
await DeviceApps.getAppFromStorage(_patcherAPI.outFile!.path);
|
||||
if (app != null) {
|
||||
_app.name = app.appName;
|
||||
_app.packageName = app.packageName;
|
||||
}
|
||||
|
||||
await _managerAPI.savePatchedApp(_app);
|
||||
|
||||
update(1.0, 'Installed!', 'Installed!');
|
||||
} else {
|
||||
// TODO(aabed): Show error message.
|
||||
}
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
@ -336,10 +393,6 @@ class InstallerViewModel extends BaseViewModel {
|
||||
}
|
||||
}
|
||||
|
||||
void exportLog() {
|
||||
_patcherAPI.exportPatcherLog(logs);
|
||||
}
|
||||
|
||||
Future<void> cleanPatcher() async {
|
||||
try {
|
||||
_patcherAPI.cleanPatcher();
|
||||
@ -363,7 +416,7 @@ class InstallerViewModel extends BaseViewModel {
|
||||
exportResult();
|
||||
break;
|
||||
case 1:
|
||||
exportLog();
|
||||
copyLogs();
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -385,6 +438,7 @@ class InstallerViewModel extends BaseViewModel {
|
||||
} else {
|
||||
_patcherAPI.cleanPatcher();
|
||||
}
|
||||
screenshotCallback.dispose();
|
||||
Navigator.of(context).pop();
|
||||
return true;
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
// 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:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
@ -18,25 +19,35 @@ class NavigationViewModel extends IndexTrackingViewModel {
|
||||
Future<void> initialize(BuildContext context) async {
|
||||
locator<Toast>().initialize(context);
|
||||
final SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
requestManageExternalStorage();
|
||||
await requestManageExternalStorage();
|
||||
|
||||
if (prefs.getBool('permissionsRequested') == null) {
|
||||
await Permission.storage.request();
|
||||
await Permission.manageExternalStorage.request();
|
||||
await prefs.setBool('permissionsRequested', true);
|
||||
RootAPI().hasRootPermissions().then(
|
||||
await RootAPI().hasRootPermissions().then(
|
||||
(value) => Permission.requestInstallPackages.request().then(
|
||||
(value) => Permission.ignoreBatteryOptimizations.request(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (prefs.getBool('useDarkTheme') == null) {
|
||||
final bool isDark =
|
||||
MediaQuery.platformBrightnessOf(context) != Brightness.light;
|
||||
await prefs.setBool('useDarkTheme', isDark);
|
||||
await DynamicTheme.of(context)!.setTheme(isDark ? 1 : 0);
|
||||
final dynamicTheme = DynamicTheme.of(context)!;
|
||||
if (prefs.getInt('themeMode') == null) {
|
||||
await prefs.setInt('themeMode', 0);
|
||||
await dynamicTheme.setTheme(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.setSystemUIOverlayStyle(
|
||||
SystemUiOverlayStyle(
|
||||
|
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(
|
||||
label: I18nText('patcherView.patchButton'),
|
||||
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(
|
||||
@ -45,7 +52,10 @@ class PatcherView extends StatelessWidget {
|
||||
delegate: SliverChildListDelegate.fixed(
|
||||
<Widget>[
|
||||
AppSelectorCard(
|
||||
onPressed: () => model.navigateToAppSelector(),
|
||||
onPressed: () => {
|
||||
model.navigateToAppSelector(),
|
||||
model.ctx = context,
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Opacity(
|
||||
|
@ -21,6 +21,7 @@ class PatcherViewModel extends BaseViewModel {
|
||||
final ManagerAPI _managerAPI = locator<ManagerAPI>();
|
||||
final PatcherAPI _patcherAPI = locator<PatcherAPI>();
|
||||
PatchedApplication? selectedApp;
|
||||
BuildContext? ctx;
|
||||
List<Patch> selectedPatches = [];
|
||||
List<String> removedPatches = [];
|
||||
|
||||
@ -44,52 +45,9 @@ class PatcherViewModel extends BaseViewModel {
|
||||
return selectedApp == null;
|
||||
}
|
||||
|
||||
Future<bool> isValidPatchConfig() async {
|
||||
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 {
|
||||
bool showRemovedPatchesDialog(BuildContext context) {
|
||||
if (removedPatches.isNotEmpty) {
|
||||
return showDialog(
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: I18nText('notice'),
|
||||
@ -102,21 +60,58 @@ class PatcherViewModel extends BaseViewModel {
|
||||
CustomMaterialButton(
|
||||
isFilled: false,
|
||||
label: I18nText('noButton'),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
CustomMaterialButton(
|
||||
label: I18nText('yesButton'),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
navigateToInstaller();
|
||||
showArmv7WarningDialog(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
showArmv7WarningDialog(context);
|
||||
return false;
|
||||
}
|
||||
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 {
|
||||
@ -185,9 +180,9 @@ class PatcherViewModel extends BaseViewModel {
|
||||
this.selectedPatches.clear();
|
||||
removedPatches.clear();
|
||||
final List<String> selectedPatches =
|
||||
await _managerAPI.getSelectedPatches(selectedApp!.originalPackageName);
|
||||
await _managerAPI.getSelectedPatches(selectedApp!.packageName);
|
||||
final List<Patch> patches =
|
||||
_patcherAPI.getFilteredPatches(selectedApp!.originalPackageName);
|
||||
_patcherAPI.getFilteredPatches(selectedApp!.packageName);
|
||||
this
|
||||
.selectedPatches
|
||||
.addAll(patches.where((patch) => selectedPatches.contains(patch.name)));
|
||||
@ -203,10 +198,13 @@ class PatcherViewModel extends BaseViewModel {
|
||||
.selectedPatches
|
||||
.removeWhere((patch) => patch.compatiblePackages.isEmpty);
|
||||
}
|
||||
final usedPatches = _managerAPI.getUsedPatches(selectedApp!.originalPackageName);
|
||||
final usedPatches = _managerAPI.getUsedPatches(selectedApp!.packageName);
|
||||
for (final patch in usedPatches){
|
||||
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();
|
||||
|
@ -37,7 +37,6 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
|
||||
onViewModelReady: (model) => model.initialize(),
|
||||
viewModelBuilder: () => PatchesSelectorViewModel(),
|
||||
builder: (context, model, child) => Scaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
floatingActionButton: Visibility(
|
||||
visible: model.patches.isNotEmpty,
|
||||
child: FloatingActionButton.extended(
|
||||
@ -49,8 +48,10 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
|
||||
),
|
||||
icon: const Icon(Icons.check),
|
||||
onPressed: () {
|
||||
model.selectPatches();
|
||||
Navigator.of(context).pop();
|
||||
if (!model.areRequiredOptionsNull(context)) {
|
||||
model.selectPatches();
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
@ -73,7 +74,10 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
|
||||
Icons.arrow_back,
|
||||
color: Theme.of(context).textTheme.titleLarge!.color,
|
||||
),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
onPressed: () {
|
||||
model.resetSelection();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
FittedBox(
|
||||
@ -114,7 +118,7 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
|
||||
),
|
||||
],
|
||||
bottom: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(64.0),
|
||||
preferredSize: const Size.fromHeight(66.0),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 8.0,
|
||||
@ -188,25 +192,121 @@ 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(
|
||||
(patch) {
|
||||
if (patch.compatiblePackages.isNotEmpty) {
|
||||
return PatchItem(
|
||||
name: patch.name,
|
||||
simpleName: patch.getSimpleName(),
|
||||
description: patch.description,
|
||||
description: patch.description ?? '',
|
||||
packageVersion: model.getAppInfo().version,
|
||||
supportedPackageVersions:
|
||||
model.getSupportedVersions(patch),
|
||||
isUnsupported: !isPatchSupported(patch),
|
||||
isChangeEnabled: _managerAPI.isPatchesChangeEnabled(),
|
||||
isNew: model.isPatchNew(
|
||||
patch,
|
||||
model.getAppInfo().packageName,
|
||||
),
|
||||
isChangeEnabled:
|
||||
_managerAPI.isPatchesChangeEnabled(),
|
||||
hasUnsupportedPatchOption:
|
||||
hasUnsupportedRequiredOption(
|
||||
patch.options, patch),
|
||||
options: patch.options,
|
||||
isSelected: model.isSelected(patch),
|
||||
onChanged: (value) =>
|
||||
model.selectPatch(patch, value, context),
|
||||
navigateToOptions: (options) =>
|
||||
model.navigateToPatchOptions(
|
||||
options,
|
||||
patch,
|
||||
),
|
||||
onChanged: (value) => model.selectPatch(
|
||||
patch,
|
||||
value,
|
||||
context,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return Container();
|
||||
@ -221,8 +321,23 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 10.0,
|
||||
),
|
||||
child: I18nText(
|
||||
'patchesSelectorView.universalPatches',
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 10.0,
|
||||
bottom: 10.0,
|
||||
left: 5.0,
|
||||
),
|
||||
child: I18nText(
|
||||
'patchesSelectorView.universalPatches',
|
||||
child: Text(
|
||||
'',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
...model.getQueriedPatches(_query).map((patch) {
|
||||
@ -230,15 +345,26 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
|
||||
return PatchItem(
|
||||
name: patch.name,
|
||||
simpleName: patch.getSimpleName(),
|
||||
description: patch.description,
|
||||
description: patch.description ?? '',
|
||||
packageVersion:
|
||||
model.getAppInfo().version,
|
||||
supportedPackageVersions:
|
||||
model.getSupportedVersions(patch),
|
||||
isUnsupported: !isPatchSupported(patch),
|
||||
isChangeEnabled: _managerAPI.isPatchesChangeEnabled(),
|
||||
isNew: false,
|
||||
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,
|
||||
|
@ -2,6 +2,7 @@ import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_i18n/widgets/I18nText.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/patched_application.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/utils/check_for_supported_patch.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
import 'package:stacked_services/stacked_services.dart';
|
||||
|
||||
class PatchesSelectorViewModel extends BaseViewModel {
|
||||
final PatcherAPI _patcherAPI = locator<PatcherAPI>();
|
||||
final ManagerAPI _managerAPI = locator<ManagerAPI>();
|
||||
final NavigationService _navigationService = locator<NavigationService>();
|
||||
final List<Patch> patches = [];
|
||||
final List<Patch> currentSelection = [];
|
||||
final List<Patch> selectedPatches =
|
||||
locator<PatcherViewModel>().selectedPatches;
|
||||
PatchedApplication? selectedApp = locator<PatcherViewModel>().selectedApp;
|
||||
@ -28,17 +32,21 @@ class PatchesSelectorViewModel extends BaseViewModel {
|
||||
getPatchesVersion().whenComplete(() => notifyListeners());
|
||||
patches.addAll(
|
||||
_patcherAPI.getFilteredPatches(
|
||||
selectedApp!.originalPackageName,
|
||||
selectedApp!.packageName,
|
||||
),
|
||||
);
|
||||
final List<Option> requiredNullOptions =
|
||||
getNullRequiredOptions(patches, selectedApp!.packageName);
|
||||
patches.sort((a, b) {
|
||||
if (isPatchNew(a, selectedApp!.packageName) ==
|
||||
isPatchNew(b, selectedApp!.packageName)) {
|
||||
return a.name.compareTo(b.name);
|
||||
if (b.options.any((option) => requiredNullOptions.contains(option)) &&
|
||||
a.options.isEmpty) {
|
||||
return 1;
|
||||
} else {
|
||||
return isPatchNew(b, selectedApp!.packageName) ? 1 : -1;
|
||||
return a.name.compareTo(b.name);
|
||||
}
|
||||
});
|
||||
currentSelection.clear();
|
||||
currentSelection.addAll(selectedPatches);
|
||||
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) {
|
||||
if (_managerAPI.isPatchesChangeEnabled()) {
|
||||
if (isSelected && !selectedPatches.contains(patch)) {
|
||||
@ -98,11 +160,11 @@ class PatchesSelectorViewModel extends BaseViewModel {
|
||||
|
||||
void selectDefaultPatches() {
|
||||
selectedPatches.clear();
|
||||
if (locator<PatcherViewModel>().selectedApp?.originalPackageName != null) {
|
||||
if (locator<PatcherViewModel>().selectedApp?.packageName != null) {
|
||||
selectedPatches.addAll(
|
||||
_patcherAPI
|
||||
.getFilteredPatches(
|
||||
locator<PatcherViewModel>().selectedApp!.originalPackageName,
|
||||
locator<PatcherViewModel>().selectedApp!.packageName,
|
||||
)
|
||||
.where(
|
||||
(element) =>
|
||||
@ -123,9 +185,19 @@ class PatchesSelectorViewModel extends BaseViewModel {
|
||||
void selectPatches() {
|
||||
locator<PatcherViewModel>().selectedPatches = selectedPatches;
|
||||
saveSelectedPatches();
|
||||
if (_managerAPI.ctx != null) {
|
||||
Navigator.pop(_managerAPI.ctx!);
|
||||
_managerAPI.ctx = null;
|
||||
}
|
||||
locator<PatcherViewModel>().notifyListeners();
|
||||
}
|
||||
|
||||
void resetSelection() {
|
||||
selectedPatches.clear();
|
||||
selectedPatches.addAll(currentSelection);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> getPatchesVersion() async {
|
||||
patchesVersion = await _managerAPI.getCurrentPatchesVersion();
|
||||
}
|
||||
@ -153,8 +225,9 @@ class PatchesSelectorViewModel extends BaseViewModel {
|
||||
return locator<PatcherViewModel>().selectedApp!;
|
||||
}
|
||||
|
||||
bool isPatchNew(Patch patch, String packageName) {
|
||||
final List<Patch> savedPatches = _managerAPI.getSavedPatches(packageName);
|
||||
bool isPatchNew(Patch patch) {
|
||||
final List<Patch> savedPatches =
|
||||
_managerAPI.getSavedPatches(selectedApp!.packageName);
|
||||
if (savedPatches.isEmpty) {
|
||||
return false;
|
||||
} else {
|
||||
@ -163,6 +236,12 @@ class PatchesSelectorViewModel extends BaseViewModel {
|
||||
}
|
||||
}
|
||||
|
||||
bool newPatchExists() {
|
||||
return patches.any(
|
||||
(patch) => isPatchNew(patch),
|
||||
);
|
||||
}
|
||||
|
||||
List<String> getSupportedVersions(Patch patch) {
|
||||
final PatchedApplication app = locator<PatcherViewModel>().selectedApp!;
|
||||
final Package? package = patch.compatiblePackages.firstWhereOrNull(
|
||||
@ -187,7 +266,7 @@ class PatchesSelectorViewModel extends BaseViewModel {
|
||||
final List<String> selectedPatches =
|
||||
this.selectedPatches.map((patch) => patch.name).toList();
|
||||
await _managerAPI.setSelectedPatches(
|
||||
locator<PatcherViewModel>().selectedApp!.originalPackageName,
|
||||
locator<PatcherViewModel>().selectedApp!.packageName,
|
||||
selectedPatches,
|
||||
);
|
||||
}
|
||||
@ -195,7 +274,7 @@ class PatchesSelectorViewModel extends BaseViewModel {
|
||||
Future<void> loadSelectedPatches(BuildContext context) async {
|
||||
if (_managerAPI.isPatchesChangeEnabled()) {
|
||||
final List<String> selectedPatches = await _managerAPI.getSelectedPatches(
|
||||
locator<PatcherViewModel>().selectedApp!.originalPackageName,
|
||||
locator<PatcherViewModel>().selectedApp!.packageName,
|
||||
);
|
||||
if (selectedPatches.isNotEmpty) {
|
||||
this.selectedPatches.clear();
|
||||
|
@ -8,6 +8,7 @@ import 'package:revanced_manager/app/app.locator.dart';
|
||||
import 'package:revanced_manager/services/manager_api.dart';
|
||||
import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/widgets/settingsView/settings_section.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
|
||||
final _settingViewModel = SettingsViewModel();
|
||||
@ -24,37 +25,114 @@ class SUpdateTheme extends BaseViewModel {
|
||||
|
||||
Future<void> setUseDynamicTheme(BuildContext context, bool value) async {
|
||||
await _managerAPI.setUseDynamicTheme(value);
|
||||
final int currentTheme = DynamicTheme.of(context)!.themeId;
|
||||
if (currentTheme.isEven) {
|
||||
await DynamicTheme.of(context)!.setTheme(value ? 2 : 0);
|
||||
} else {
|
||||
await DynamicTheme.of(context)!.setTheme(value ? 3 : 1);
|
||||
}
|
||||
final int currentTheme = (DynamicTheme.of(context)!.themeId ~/ 2) * 2;
|
||||
await DynamicTheme.of(context)!.setTheme(currentTheme + (value ? 1 : 0));
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool getDarkThemeStatus() {
|
||||
return _managerAPI.getUseDarkTheme();
|
||||
int getThemeMode() {
|
||||
return _managerAPI.getThemeMode();
|
||||
}
|
||||
|
||||
Future<void> setUseDarkTheme(BuildContext context, bool value) async {
|
||||
await _managerAPI.setUseDarkTheme(value);
|
||||
final int currentTheme = DynamicTheme.of(context)!.themeId;
|
||||
if (currentTheme < 2) {
|
||||
await DynamicTheme.of(context)!.setTheme(value ? 1 : 0);
|
||||
} else {
|
||||
await DynamicTheme.of(context)!.setTheme(value ? 3 : 2);
|
||||
}
|
||||
Future<void> setThemeMode(BuildContext context, int value) async {
|
||||
await _managerAPI.setThemeMode(value);
|
||||
final bool isDynamicTheme = DynamicTheme.of(context)!.themeId.isEven;
|
||||
await DynamicTheme.of(context)!.setTheme(value * 2 + (isDynamicTheme ? 0 : 1));
|
||||
final bool isLight = value != 2 && (value == 1 || DynamicTheme.of(context)!.theme.brightness == Brightness.light);
|
||||
SystemChrome.setSystemUIOverlayStyle(
|
||||
SystemUiOverlayStyle(
|
||||
systemNavigationBarIconBrightness:
|
||||
value ? Brightness.light : Brightness.dark,
|
||||
isLight ? Brightness.dark : Brightness.light,
|
||||
),
|
||||
);
|
||||
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 {
|
||||
const SUpdateThemeUI({super.key});
|
||||
|
||||
@ -63,10 +141,10 @@ class SUpdateThemeUI extends StatelessWidget {
|
||||
return SettingsSection(
|
||||
title: 'settingsView.appearanceSectionTitle',
|
||||
children: <Widget>[
|
||||
SwitchListTile(
|
||||
ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
title: I18nText(
|
||||
'settingsView.darkThemeLabel',
|
||||
'settingsView.themeModeLabel',
|
||||
child: const Text(
|
||||
'',
|
||||
style: TextStyle(
|
||||
@ -75,12 +153,11 @@ class SUpdateThemeUI extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
subtitle: I18nText('settingsView.darkThemeHint'),
|
||||
value: SUpdateTheme().getDarkThemeStatus(),
|
||||
onChanged: (value) => SUpdateTheme().setUseDarkTheme(
|
||||
context,
|
||||
value,
|
||||
trailing: CustomMaterialButton(
|
||||
label: sUpdateTheme.getThemeModeName(),
|
||||
onPressed: () => { sUpdateTheme.showThemeDialog(context) },
|
||||
),
|
||||
onTap: () => { sUpdateTheme.showThemeDialog(context) },
|
||||
),
|
||||
FutureBuilder<int>(
|
||||
future: _settingViewModel.getSdkVersion(),
|
||||
|
@ -71,12 +71,6 @@ class SettingsViewModel extends BaseViewModel {
|
||||
actions: [
|
||||
CustomMaterialButton(
|
||||
isFilled: false,
|
||||
label: I18nText('noButton'),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
CustomMaterialButton(
|
||||
label: I18nText('yesButton'),
|
||||
onPressed: () {
|
||||
_managerAPI.setChangingToggleModified(true);
|
||||
@ -84,6 +78,12 @@ class SettingsViewModel extends BaseViewModel {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
CustomMaterialButton(
|
||||
label: I18nText('noButton'),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@ -246,6 +246,11 @@ class SettingsViewModel extends BaseViewModel {
|
||||
}
|
||||
}
|
||||
|
||||
void resetAllOptions() {
|
||||
_managerAPI.resetAllOptions();
|
||||
_toast.showBottom('settingsView.resetStoredOptions');
|
||||
}
|
||||
|
||||
void resetSelectedPatches() {
|
||||
_managerAPI.resetLastSelectedPatches();
|
||||
_toast.showBottom('settingsView.resetStoredPatches');
|
||||
|
@ -222,22 +222,6 @@ class AppInfoView extends StatelessWidget {
|
||||
subtitle: Text(app.packageName),
|
||||
),
|
||||
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(
|
||||
contentPadding:
|
||||
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/patcher/patcher_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
|
||||
import 'package:revanced_manager/utils/string.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
|
||||
class AppInfoViewModel extends BaseViewModel {
|
||||
@ -147,17 +146,7 @@ class AppInfoViewModel extends BaseViewModel {
|
||||
}
|
||||
|
||||
String getAppliedPatchesString(List<String> appliedPatches) {
|
||||
final List<String> names = appliedPatches
|
||||
.map(
|
||||
(p) => p
|
||||
.replaceAll('-', ' ')
|
||||
.split('-')
|
||||
.join(' ')
|
||||
.toTitleCase()
|
||||
.replaceFirst('Microg', 'MicroG'),
|
||||
)
|
||||
.toList();
|
||||
return '\u2022 ${names.join('\n\u2022 ')}';
|
||||
return '• ${appliedPatches.join('\n• ')}';
|
||||
}
|
||||
|
||||
void openApp(PatchedApplication app) {
|
||||
|
@ -79,8 +79,6 @@ class InstalledAppsCard extends StatelessWidget {
|
||||
icon: app.icon,
|
||||
name: app.name,
|
||||
patchDate: app.patchDate,
|
||||
changelog: app.changelog,
|
||||
isUpdatableApp: false,
|
||||
onPressed: () =>
|
||||
locator<HomeViewModel>().navigateToAppInfo(app),
|
||||
),
|
||||
|
@ -58,8 +58,10 @@ class PatchSelectorCard extends StatelessWidget {
|
||||
|
||||
String _getPatchesSelection() {
|
||||
String text = '';
|
||||
for (final Patch p in locator<PatcherViewModel>().selectedPatches) {
|
||||
text += '\u2022 ${p.getSimpleName()}\n';
|
||||
final List<Patch> selectedPatches = locator<PatcherViewModel>().selectedPatches;
|
||||
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);
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_i18n/flutter_i18n.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/toast.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
|
||||
@ -16,11 +17,12 @@ class PatchItem extends StatefulWidget {
|
||||
required this.packageVersion,
|
||||
required this.supportedPackageVersions,
|
||||
required this.isUnsupported,
|
||||
required this.isNew,
|
||||
required this.hasUnsupportedPatchOption,
|
||||
required this.options,
|
||||
required this.isSelected,
|
||||
required this.onChanged,
|
||||
required this.navigateToOptions,
|
||||
required this.isChangeEnabled,
|
||||
this.child,
|
||||
}) : super(key: key);
|
||||
final String name;
|
||||
final String simpleName;
|
||||
@ -28,11 +30,12 @@ class PatchItem extends StatefulWidget {
|
||||
final String packageVersion;
|
||||
final List<String> supportedPackageVersions;
|
||||
final bool isUnsupported;
|
||||
final bool isNew;
|
||||
final bool hasUnsupportedPatchOption;
|
||||
final List<Option> options;
|
||||
bool isSelected;
|
||||
final Function(bool) onChanged;
|
||||
final void Function(List<Option>) navigateToOptions;
|
||||
final bool isChangeEnabled;
|
||||
final Widget? child;
|
||||
final toast = locator<Toast>();
|
||||
final _managerAPI = locator<ManagerAPI>();
|
||||
|
||||
@ -45,7 +48,8 @@ class _PatchItemState extends State<PatchItem> {
|
||||
Widget build(BuildContext context) {
|
||||
widget.isSelected = widget.isSelected &&
|
||||
(!widget.isUnsupported ||
|
||||
widget._managerAPI.areExperimentalPatchesEnabled());
|
||||
widget._managerAPI.areExperimentalPatchesEnabled()) &&
|
||||
!widget.hasUnsupportedPatchOption;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
||||
child: Opacity(
|
||||
@ -54,148 +58,150 @@ class _PatchItemState extends State<PatchItem> {
|
||||
? 0.5
|
||||
: 1,
|
||||
child: CustomCard(
|
||||
padding: EdgeInsets.only(
|
||||
top: 12,
|
||||
bottom: 16,
|
||||
left: 8.0,
|
||||
right: widget.options.isNotEmpty ? 4.0 : 8.0,
|
||||
),
|
||||
onTap: () {
|
||||
setState(() {
|
||||
if (widget.isUnsupported &&
|
||||
!widget._managerAPI.areExperimentalPatchesEnabled()) {
|
||||
widget.isSelected = false;
|
||||
widget.toast.showBottom('patchItem.unsupportedPatchVersion');
|
||||
} else if (widget.isChangeEnabled) {
|
||||
widget.isSelected = !widget.isSelected;
|
||||
if (widget.isUnsupported &&
|
||||
!widget._managerAPI.areExperimentalPatchesEnabled()) {
|
||||
widget.isSelected = false;
|
||||
widget.toast.showBottom('patchItem.unsupportedPatchVersion');
|
||||
} else if (widget.isChangeEnabled) {
|
||||
if (!widget.isSelected) {
|
||||
if (widget.hasUnsupportedPatchOption) {
|
||||
_showUnsupportedRequiredOptionDialog();
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
if (!widget.isUnsupported || widget._managerAPI.areExperimentalPatchesEnabled()) {
|
||||
widget.isSelected = !widget.isSelected;
|
||||
setState(() {});
|
||||
}
|
||||
if (!widget.isUnsupported ||
|
||||
widget._managerAPI.areExperimentalPatchesEnabled()) {
|
||||
widget.onChanged(widget.isSelected);
|
||||
}
|
||||
},
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Flexible(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: <Widget>[
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Transform.scale(
|
||||
scale: 1.2,
|
||||
child: Checkbox(
|
||||
value: widget.isSelected,
|
||||
activeColor: Theme.of(context).colorScheme.primary,
|
||||
checkColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
side: BorderSide(
|
||||
width: 2.0,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
Transform.scale(
|
||||
scale: 1.2,
|
||||
child: Checkbox(
|
||||
value: widget.isSelected,
|
||||
activeColor: Theme.of(context).colorScheme.primary,
|
||||
checkColor:
|
||||
Theme.of(context).colorScheme.secondaryContainer,
|
||||
side: BorderSide(
|
||||
width: 2.0,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
onChanged: (newValue) {
|
||||
setState(() {
|
||||
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);
|
||||
onChanged: (newValue) {
|
||||
if (widget.isUnsupported &&
|
||||
!widget._managerAPI.areExperimentalPatchesEnabled()) {
|
||||
widget.isSelected = false;
|
||||
widget.toast.showBottom(
|
||||
'patchItem.unsupportedPatchVersion',
|
||||
);
|
||||
} else if (widget.isChangeEnabled) {
|
||||
if (!widget.isSelected) {
|
||||
if (widget.hasUnsupportedPatchOption) {
|
||||
_showUnsupportedRequiredOptionDialog();
|
||||
return;
|
||||
}
|
||||
},
|
||||
),
|
||||
}
|
||||
widget.isSelected = newValue!;
|
||||
setState(() {});
|
||||
}
|
||||
if (!widget.isUnsupported ||
|
||||
widget._managerAPI.areExperimentalPatchesEnabled()) {
|
||||
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
|
||||
.areExperimentalPatchesEnabled())
|
||||
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(
|
||||
children: [
|
||||
if (widget.isUnsupported &&
|
||||
widget._managerAPI.areExperimentalPatchesEnabled())
|
||||
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(),
|
||||
if (widget.options.isNotEmpty)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.settings_outlined),
|
||||
onPressed: () => widget.navigateToOptions(widget.options),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -214,7 +220,7 @@ class _PatchItemState extends State<PatchItem> {
|
||||
translationParams: {
|
||||
'packageVersion': widget.packageVersion,
|
||||
'supportedVersions':
|
||||
'\u2022 ${widget.supportedPackageVersions.reversed.join('\n\u2022 ')}',
|
||||
'• ${widget.supportedPackageVersions.reversed.join('\n• ')}',
|
||||
},
|
||||
),
|
||||
actions: <Widget>[
|
||||
@ -227,14 +233,14 @@ class _PatchItemState extends State<PatchItem> {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showNewPatchDialog() {
|
||||
Future<void> _showUnsupportedRequiredOptionDialog() {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: I18nText('patchItem.newPatch'),
|
||||
title: I18nText('notice'),
|
||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
content: I18nText(
|
||||
'patchItem.newPatchDialogText',
|
||||
'patchItem.unsupportedRequiredOption',
|
||||
),
|
||||
actions: <Widget>[
|
||||
CustomMaterialButton(
|
||||
|
@ -1,73 +1,387 @@
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.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 {
|
||||
const OptionsTextField({Key? key, required this.hint}) : super(key: key);
|
||||
final String hint;
|
||||
class BooleanPatchOption extends StatelessWidget {
|
||||
const BooleanPatchOption({
|
||||
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 size = MediaQuery.sizeOf(context);
|
||||
final sHeight = size.height;
|
||||
final sWidth = size.width;
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(top: 12, bottom: 6),
|
||||
padding: EdgeInsets.zero,
|
||||
child: TextField(
|
||||
decoration: InputDecoration(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: sHeight * 0.05,
|
||||
maxWidth: sWidth * 1,
|
||||
final ValueNotifier patchOptionValue = ValueNotifier(patchOption.value);
|
||||
return PatchOption(
|
||||
widget: Align(
|
||||
alignment: Alignment.bottomLeft,
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: patchOptionValue,
|
||||
builder: (context, value, child) {
|
||||
return Switch(
|
||||
value: value ?? false,
|
||||
onChanged: (bool value) {
|
||||
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(),
|
||||
labelText: hint,
|
||||
ValueListenableBuilder(
|
||||
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(
|
||||
onTap: () {},
|
||||
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 {
|
||||
const OptionsFilePicker({Key? key, required this.optionName})
|
||||
: super(key: key);
|
||||
final String optionName;
|
||||
class TextFieldForPatchOption extends StatefulWidget {
|
||||
const TextFieldForPatchOption({
|
||||
super.key,
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
I18nText(
|
||||
optionName,
|
||||
child: Text(
|
||||
'',
|
||||
style: GoogleFonts.inter(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
final bool isStringOption = widget.optionType.contains('String');
|
||||
final bool isListOption = widget.optionType.contains('List');
|
||||
controller.text = widget.value ?? '';
|
||||
return TextFormField(
|
||||
inputFormatters: [
|
||||
if (widget.optionType.contains('Int'))
|
||||
FilteringTextInputFormatter.allow(RegExp(r'[0-9]')),
|
||||
if (widget.optionType.contains('Long'))
|
||||
FilteringTextInputFormatter.allow(RegExp(r'^[0-9]*\.?[0-9]*')),
|
||||
],
|
||||
controller: controller,
|
||||
keyboardType: isStringOption ? TextInputType.text : TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
suffixIcon: PopupMenuButton(
|
||||
tooltip: FlutterI18n.translate(
|
||||
context,
|
||||
'patchOptionsView.tooltip',
|
||||
),
|
||||
ElevatedButton(
|
||||
style: ButtonStyle(
|
||||
backgroundColor: MaterialStateProperty.all(
|
||||
Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
// pick files
|
||||
},
|
||||
child: Text(
|
||||
'Select File',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).textTheme.bodyLarge?.color,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
itemBuilder: (BuildContext context) {
|
||||
return [
|
||||
if (isListOption)
|
||||
PopupMenuItem(
|
||||
value: 'remove',
|
||||
child: I18nText('remove'),
|
||||
),
|
||||
if (isStringOption && !isListOption) ...[
|
||||
PopupMenuItem(
|
||||
value: 'patchOptionsView.selectFilePath',
|
||||
child: I18nText('patchOptionsView.selectFilePath'),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: 'patchOptionsView.selectFolder',
|
||||
child: I18nText('patchOptionsView.selectFolder'),
|
||||
),
|
||||
],
|
||||
];
|
||||
},
|
||||
onSelected: (String selection) async {
|
||||
switch (selection) {
|
||||
case 'patchOptionsView.selectFilePath':
|
||||
final result = await FilePicker.platform.pickFiles();
|
||||
if (result != null && result.files.single.path != null) {
|
||||
controller.text = result.files.single.path.toString();
|
||||
widget.onChanged(controller.text);
|
||||
}
|
||||
break;
|
||||
case 'patchOptionsView.selectFolder':
|
||||
final result = await FilePicker.platform.getDirectoryPath();
|
||||
if (result != null) {
|
||||
controller.text = result;
|
||||
widget.onChanged(controller.text);
|
||||
}
|
||||
break;
|
||||
case 'remove':
|
||||
widget.removeValue!(widget.value);
|
||||
break;
|
||||
}
|
||||
},
|
||||
),
|
||||
hintStyle: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Theme.of(context).colorScheme.onSecondaryContainer,
|
||||
),
|
||||
),
|
||||
onChanged: (String value) {
|
||||
widget.onChanged(value);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -5,8 +5,8 @@ import 'package:flutter_i18n/widgets/I18nText.dart';
|
||||
import 'package:revanced_manager/ui/views/settings/settingsFragment/settings_manage_api_url.dart';
|
||||
import 'package:revanced_manager/ui/views/settings/settingsFragment/settings_manage_sources.dart';
|
||||
import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/widgets/settingsView/settings_enable_patches_selection.dart';
|
||||
import 'package:revanced_manager/ui/widgets/settingsView/settings_auto_update_patches.dart';
|
||||
import 'package:revanced_manager/ui/widgets/settingsView/settings_enable_patches_selection.dart';
|
||||
import 'package:revanced_manager/ui/widgets/settingsView/settings_experimental_patches.dart';
|
||||
import 'package:revanced_manager/ui/widgets/settingsView/settings_experimental_universal_patches.dart';
|
||||
import 'package:revanced_manager/ui/widgets/settingsView/settings_section.dart';
|
||||
|
@ -94,21 +94,49 @@ class SExportSection extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
subtitle: I18nText('settingsView.resetStoredPatchesHint'),
|
||||
onTap: () => _showResetStoredPatchesDialog(context),
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showResetStoredPatchesDialog(context) {
|
||||
Future<void> _showResetDialog(
|
||||
context,
|
||||
dialogTitle,
|
||||
dialogText,
|
||||
dialogAction,
|
||||
) {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: I18nText('settingsView.resetStoredPatchesDialogTitle'),
|
||||
title: I18nText(dialogTitle),
|
||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
content: I18nText(
|
||||
'settingsView.resetStoredPatchesDialogText',
|
||||
),
|
||||
content: I18nText(dialogText),
|
||||
actions: <Widget>[
|
||||
CustomMaterialButton(
|
||||
isFilled: false,
|
||||
@ -119,7 +147,7 @@ class SExportSection extends StatelessWidget {
|
||||
label: I18nText('yesButton'),
|
||||
onPressed: () => {
|
||||
Navigator.of(context).pop(),
|
||||
_settingsViewModel.resetSelectedPatches(),
|
||||
dialogAction(),
|
||||
},
|
||||
),
|
||||
],
|
||||
|
@ -75,8 +75,8 @@ class SocialMediaWidget extends StatelessWidget {
|
||||
SocialMediaItem(
|
||||
icon: FaIcon(FontAwesomeIcons.youtube),
|
||||
title: Text('YouTube'),
|
||||
subtitle: Text('youtube.com/revanced'),
|
||||
url: 'https://youtube.com/revanced',
|
||||
subtitle: Text('youtube.com/@revanced'),
|
||||
url: 'https://youtube.com/@revanced',
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -1,6 +1,5 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:expandable/expandable.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_i18n/flutter_i18n.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
|
||||
@ -13,151 +12,84 @@ class ApplicationItem extends StatefulWidget {
|
||||
required this.icon,
|
||||
required this.name,
|
||||
required this.patchDate,
|
||||
required this.changelog,
|
||||
required this.isUpdatableApp,
|
||||
required this.onPressed,
|
||||
}) : super(key: key);
|
||||
final Uint8List icon;
|
||||
final String name;
|
||||
final DateTime patchDate;
|
||||
final List<String> changelog;
|
||||
final bool isUpdatableApp;
|
||||
final Function() onPressed;
|
||||
|
||||
@override
|
||||
State<ApplicationItem> createState() => _ApplicationItemState();
|
||||
}
|
||||
|
||||
class _ApplicationItemState extends State<ApplicationItem>
|
||||
with TickerProviderStateMixin {
|
||||
late AnimationController _animationController;
|
||||
class _ApplicationItemState extends State<ApplicationItem> {
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_animationController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_animationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ExpandableController expController = ExpandableController();
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 16.0),
|
||||
child: CustomCard(
|
||||
onTap: () {
|
||||
expController.toggle();
|
||||
_animationController.isCompleted
|
||||
? _animationController.reverse()
|
||||
: _animationController.forward();
|
||||
},
|
||||
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(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Row(
|
||||
children: [
|
||||
RotationTransition(
|
||||
turns: Tween(begin: 0.0, end: 0.50)
|
||||
.animate(_animationController),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: Icon(Icons.arrow_drop_down),
|
||||
),
|
||||
SizedBox(
|
||||
width: 40,
|
||||
child: Image.memory(widget.icon, height: 40, width: 40),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: <Widget>[
|
||||
CustomMaterialButton(
|
||||
label: widget.isUpdatableApp
|
||||
? I18nText('applicationItem.patchButton')
|
||||
: I18nText('applicationItem.infoButton'),
|
||||
onPressed: widget.onPressed,
|
||||
),
|
||||
],
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
collapsed: const SizedBox(),
|
||||
expanded: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 16.0,
|
||||
left: 4.0,
|
||||
right: 4.0,
|
||||
bottom: 4.0,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
I18nText(
|
||||
'applicationItem.changelogLabel',
|
||||
child: const Text(
|
||||
'',
|
||||
style: TextStyle(fontWeight: FontWeight.w700),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
const SizedBox(width: 8),
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: <Widget>[
|
||||
CustomMaterialButton(
|
||||
label: I18nText('applicationItem.infoButton'),
|
||||
onPressed: widget.onPressed,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text('\u2022 ${widget.changelog.join('\n\u2022 ')}'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'package:revanced_manager/app/app.locator.dart';
|
||||
import 'package:revanced_manager/models/patch.dart';
|
||||
import 'package:revanced_manager/models/patched_application.dart';
|
||||
import 'package:revanced_manager/services/manager_api.dart';
|
||||
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
|
||||
|
||||
bool isPatchSupported(Patch patch) {
|
||||
@ -12,3 +13,49 @@ bool isPatchSupported(Patch patch) {
|
||||
(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'
|
||||
|
||||
version: 1.9.2+100900200
|
||||
version: 1.12.0+101200000
|
||||
|
||||
environment:
|
||||
sdk: '>=3.0.0 <4.0.0'
|
||||
@ -75,6 +75,8 @@ dependencies:
|
||||
flutter_markdown: ^0.6.14
|
||||
dio_cache_interceptor: ^3.4.0
|
||||
install_plugin: ^2.1.0
|
||||
screenshot_callback: ^3.0.1
|
||||
synchronized: ^3.1.0
|
||||
|
||||
dev_dependencies:
|
||||
json_serializable: ^6.6.1
|
||||
|
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