mirror of
https://github.com/revanced/revanced-manager
synced 2024-05-14 13:56:57 +02:00
feat: Add patch options (#1354)
This commit is contained in:
parent
2abadc73e4
commit
ac636670c3
@ -85,7 +85,7 @@ dependencies {
|
|||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||||
|
|
||||||
// ReVanced
|
// ReVanced
|
||||||
implementation "app.revanced:revanced-patcher:16.0.2"
|
implementation "app.revanced:revanced-patcher:17.0.0"
|
||||||
|
|
||||||
// Signing & aligning
|
// Signing & aligning
|
||||||
implementation("org.bouncycastle:bcpkix-jdk15on:1.70")
|
implementation("org.bouncycastle:bcpkix-jdk15on:1.70")
|
||||||
|
@ -22,7 +22,6 @@ import org.json.JSONObject
|
|||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.PrintWriter
|
import java.io.PrintWriter
|
||||||
import java.io.StringWriter
|
import java.io.StringWriter
|
||||||
import java.lang.Error
|
|
||||||
import java.util.logging.LogRecord
|
import java.util.logging.LogRecord
|
||||||
import java.util.logging.Logger
|
import java.util.logging.Logger
|
||||||
|
|
||||||
@ -55,6 +54,7 @@ class MainActivity : FlutterActivity() {
|
|||||||
val outFilePath = call.argument<String>("outFilePath")
|
val outFilePath = call.argument<String>("outFilePath")
|
||||||
val integrationsPath = call.argument<String>("integrationsPath")
|
val integrationsPath = call.argument<String>("integrationsPath")
|
||||||
val selectedPatches = call.argument<List<String>>("selectedPatches")
|
val selectedPatches = call.argument<List<String>>("selectedPatches")
|
||||||
|
val options = call.argument<Map<String, Map<String, Any>>>("options")
|
||||||
val cacheDirPath = call.argument<String>("cacheDirPath")
|
val cacheDirPath = call.argument<String>("cacheDirPath")
|
||||||
val keyStoreFilePath = call.argument<String>("keyStoreFilePath")
|
val keyStoreFilePath = call.argument<String>("keyStoreFilePath")
|
||||||
val keystorePassword = call.argument<String>("keystorePassword")
|
val keystorePassword = call.argument<String>("keystorePassword")
|
||||||
@ -66,6 +66,7 @@ class MainActivity : FlutterActivity() {
|
|||||||
outFilePath != null &&
|
outFilePath != null &&
|
||||||
integrationsPath != null &&
|
integrationsPath != null &&
|
||||||
selectedPatches != null &&
|
selectedPatches != null &&
|
||||||
|
options != null &&
|
||||||
cacheDirPath != null &&
|
cacheDirPath != null &&
|
||||||
keyStoreFilePath != null &&
|
keyStoreFilePath != null &&
|
||||||
keystorePassword != null
|
keystorePassword != null
|
||||||
@ -79,6 +80,7 @@ class MainActivity : FlutterActivity() {
|
|||||||
outFilePath,
|
outFilePath,
|
||||||
integrationsPath,
|
integrationsPath,
|
||||||
selectedPatches,
|
selectedPatches,
|
||||||
|
options,
|
||||||
cacheDirPath,
|
cacheDirPath,
|
||||||
keyStoreFilePath,
|
keyStoreFilePath,
|
||||||
keystorePassword
|
keystorePassword
|
||||||
@ -127,6 +129,28 @@ class MainActivity : FlutterActivity() {
|
|||||||
put(compatiblePackageJson)
|
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)
|
}.let(::put)
|
||||||
}
|
}
|
||||||
}.toString().let(result::success)
|
}.toString().let(result::success)
|
||||||
@ -145,6 +169,7 @@ class MainActivity : FlutterActivity() {
|
|||||||
outFilePath: String,
|
outFilePath: String,
|
||||||
integrationsPath: String,
|
integrationsPath: String,
|
||||||
selectedPatches: List<String>,
|
selectedPatches: List<String>,
|
||||||
|
options: Map<String, Map<String, Any>>,
|
||||||
cacheDirPath: String,
|
cacheDirPath: String,
|
||||||
keyStoreFilePath: String,
|
keyStoreFilePath: String,
|
||||||
keystorePassword: String
|
keystorePassword: String
|
||||||
@ -182,7 +207,7 @@ class MainActivity : FlutterActivity() {
|
|||||||
|
|
||||||
object : java.util.logging.Handler() {
|
object : java.util.logging.Handler() {
|
||||||
override fun publish(record: LogRecord) {
|
override fun publish(record: LogRecord) {
|
||||||
if (record.loggerName?.startsWith("app.revanced") != true) return
|
if (record.loggerName?.startsWith("app.revanced") != true || cancel) return
|
||||||
|
|
||||||
updateProgress(-1.0, "", record.message)
|
updateProgress(-1.0, "", record.message)
|
||||||
}
|
}
|
||||||
@ -215,6 +240,7 @@ class MainActivity : FlutterActivity() {
|
|||||||
cacheDir,
|
cacheDir,
|
||||||
Aapt.binary(applicationContext).absolutePath,
|
Aapt.binary(applicationContext).absolutePath,
|
||||||
cacheDir.path,
|
cacheDir.path,
|
||||||
|
true // TODO: Add option to disable this
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -234,6 +260,10 @@ class MainActivity : FlutterActivity() {
|
|||||||
isCompatible || patch.compatiblePackages.isNullOrEmpty()
|
isCompatible || patch.compatiblePackages.isNullOrEmpty()
|
||||||
|
|
||||||
compatibleOrUniversal && selectedPatches.any { it == patch.name }
|
compatibleOrUniversal && selectedPatches.any { it == patch.name }
|
||||||
|
}.onEach { patch ->
|
||||||
|
options[patch.name]?.forEach { (key, value) ->
|
||||||
|
patch.options[key] = value
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cancel) {
|
if (cancel) {
|
||||||
|
@ -16,11 +16,7 @@ allprojects {
|
|||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
maven {
|
maven {
|
||||||
url = uri("https://maven.pkg.github.com/revanced/revanced-patcher")
|
url 'https://jitpack.io'
|
||||||
credentials {
|
|
||||||
username = (project.findProperty("gpr.user") ?: System.getenv("GITHUB_ACTOR")) as String
|
|
||||||
password = (project.findProperty("gpr.key") ?: System.getenv("GITHUB_TOKEN")) as String
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
mavenLocal()
|
mavenLocal()
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionSha256Sum=a01b6587e15fe7ed120a0ee299c25982a1eee045abd6a9dd5e216b2f628ef9ac
|
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.2-bin.zip
|
|
||||||
networkTimeout=10000
|
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip
|
||||||
|
@ -10,9 +10,11 @@
|
|||||||
"yesButton": "Yes",
|
"yesButton": "Yes",
|
||||||
"noButton": "No",
|
"noButton": "No",
|
||||||
"warning": "Warning",
|
"warning": "Warning",
|
||||||
|
"options": "Options",
|
||||||
"notice": "Notice",
|
"notice": "Notice",
|
||||||
"noShowAgain": "Don't show this again",
|
"noShowAgain": "Don't show this again",
|
||||||
"new": "New",
|
"add": "Add",
|
||||||
|
"remove": "Remove",
|
||||||
"navigationView": {
|
"navigationView": {
|
||||||
"dashboardTab": "Dashboard",
|
"dashboardTab": "Dashboard",
|
||||||
"patcherTab": "Patcher",
|
"patcherTab": "Patcher",
|
||||||
@ -71,7 +73,8 @@
|
|||||||
|
|
||||||
"armv7WarningDialogText": "Patching on ARMv7 devices is not yet supported and might fail. Proceed anyways?",
|
"armv7WarningDialogText": "Patching on ARMv7 devices is not yet supported and might fail. Proceed anyways?",
|
||||||
|
|
||||||
"removedPatchesWarningDialogText": "The following patches have been removed since the last time you used them.\n\n{patches}\n\nProceed anyways?"
|
"removedPatchesWarningDialogText": "The following patches have been removed since the last time you used them.\n\n{patches}\n\nProceed anyways?",
|
||||||
|
"requiredOptionDialogText" : "Some patch options have to be set."
|
||||||
},
|
},
|
||||||
"appSelectorCard": {
|
"appSelectorCard": {
|
||||||
"widgetTitle": "Select an application",
|
"widgetTitle": "Select an application",
|
||||||
@ -114,6 +117,8 @@
|
|||||||
"viewTitle": "Select patches",
|
"viewTitle": "Select patches",
|
||||||
"searchBarHint": "Search patches",
|
"searchBarHint": "Search patches",
|
||||||
"universalPatches": "Universal patches",
|
"universalPatches": "Universal patches",
|
||||||
|
"newPatches": "New patches",
|
||||||
|
"patches": "Patches",
|
||||||
|
|
||||||
"doneButton": "Done",
|
"doneButton": "Done",
|
||||||
|
|
||||||
@ -126,15 +131,29 @@
|
|||||||
"loadPatchesSelection": "Load patches selection",
|
"loadPatchesSelection": "Load patches selection",
|
||||||
"noSavedPatches": "No saved patches for the selected app.\nPress Done to save current selection.",
|
"noSavedPatches": "No saved patches for the selected app.\nPress Done to save current selection.",
|
||||||
"noPatchesFound": "No patches found for the selected app",
|
"noPatchesFound": "No patches found for the selected app",
|
||||||
|
"setRequiredOption": "Some patches require options to be set:\n\n{patches}\n\nPlease set them before continuing.",
|
||||||
|
|
||||||
"selectAllPatchesWarningContent": "You are about to select all patches, that includes non-suggested patches and can cause unwanted behavior."
|
"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": {
|
"patchItem": {
|
||||||
"unsupportedDialogText": "Selecting this patch may result in patching errors.\n\nApp version: {packageVersion}\nSupported versions:\n{supportedVersions}",
|
"unsupportedDialogText": "Selecting this patch may result in patching errors.\n\nApp version: {packageVersion}\nSupported versions:\n{supportedVersions}",
|
||||||
"unsupportedPatchVersion": "Patch is not supported for this app version. Enable the experimental toggle in settings to proceed.",
|
"unsupportedPatchVersion": "Patch is not supported for this app version. Enable the experimental toggle in settings to proceed.",
|
||||||
|
"unsupportedRequiredOption": "This patch contains a required option that is not supported by this app",
|
||||||
"newPatchDialogText": "This is a new patch that has been added since the last time you have patched this app.",
|
|
||||||
"newPatch": "New patch",
|
|
||||||
|
|
||||||
"patchesChangeWarningDialogText": "It is recommended to use the default selection of patches because changing it may cause unexpected issues.\n\nIf you know what you are doing, you can enable \"Enable changing selection\" in the settings.",
|
"patchesChangeWarningDialogText": "It is recommended to use the default selection of patches because changing it may cause unexpected issues.\n\nIf you know what you are doing, you can enable \"Enable changing selection\" in the settings.",
|
||||||
"patchesChangeWarningDialogButton": "Use default selection"
|
"patchesChangeWarningDialogButton": "Use default selection"
|
||||||
@ -239,10 +258,17 @@
|
|||||||
"resetStoredPatchesLabel": "Reset patches",
|
"resetStoredPatchesLabel": "Reset patches",
|
||||||
"resetStoredPatchesHint": "Reset the stored patches selection",
|
"resetStoredPatchesHint": "Reset the stored patches selection",
|
||||||
|
|
||||||
|
"resetStoredOptionsLabel": "Reset options",
|
||||||
|
"resetStoredOptionsHint": "Reset all patch options",
|
||||||
|
|
||||||
"resetStoredPatchesDialogTitle": "Reset patches selection?",
|
"resetStoredPatchesDialogTitle": "Reset patches selection?",
|
||||||
"resetStoredPatchesDialogText": "Resetting patches selection will remove all selected patches.",
|
"resetStoredPatchesDialogText": "Resetting patches selection will remove all selected patches.",
|
||||||
"resetStoredPatches": "Patches selection has been reset",
|
"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",
|
"deleteLogsLabel": "Delete logs",
|
||||||
"deleteLogsHint": "Delete collected manager logs",
|
"deleteLogsHint": "Delete collected manager logs",
|
||||||
"deletedLogs": "Logs deleted",
|
"deletedLogs": "Logs deleted",
|
||||||
|
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/installer/installer_view.dart';
|
||||||
import 'package:revanced_manager/ui/views/navigation/navigation_view.dart';
|
import 'package:revanced_manager/ui/views/navigation/navigation_view.dart';
|
||||||
import 'package:revanced_manager/ui/views/navigation/navigation_viewmodel.dart';
|
import 'package:revanced_manager/ui/views/navigation/navigation_viewmodel.dart';
|
||||||
|
import 'package:revanced_manager/ui/views/patch_options/patch_options_view.dart';
|
||||||
|
import 'package:revanced_manager/ui/views/patch_options/patch_options_viewmodel.dart';
|
||||||
import 'package:revanced_manager/ui/views/patcher/patcher_view.dart';
|
import 'package:revanced_manager/ui/views/patcher/patcher_view.dart';
|
||||||
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
|
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
|
||||||
import 'package:revanced_manager/ui/views/patches_selector/patches_selector_view.dart';
|
import 'package:revanced_manager/ui/views/patches_selector/patches_selector_view.dart';
|
||||||
@ -23,6 +25,7 @@ import 'package:stacked_services/stacked_services.dart';
|
|||||||
MaterialRoute(page: PatcherView),
|
MaterialRoute(page: PatcherView),
|
||||||
MaterialRoute(page: AppSelectorView),
|
MaterialRoute(page: AppSelectorView),
|
||||||
MaterialRoute(page: PatchesSelectorView),
|
MaterialRoute(page: PatchesSelectorView),
|
||||||
|
MaterialRoute(page: PatchOptionsView),
|
||||||
MaterialRoute(page: InstallerView),
|
MaterialRoute(page: InstallerView),
|
||||||
MaterialRoute(page: SettingsView),
|
MaterialRoute(page: SettingsView),
|
||||||
MaterialRoute(page: ContributorsView),
|
MaterialRoute(page: ContributorsView),
|
||||||
@ -32,6 +35,7 @@ import 'package:stacked_services/stacked_services.dart';
|
|||||||
LazySingleton(classType: NavigationViewModel),
|
LazySingleton(classType: NavigationViewModel),
|
||||||
LazySingleton(classType: HomeViewModel),
|
LazySingleton(classType: HomeViewModel),
|
||||||
LazySingleton(classType: PatcherViewModel),
|
LazySingleton(classType: PatcherViewModel),
|
||||||
|
LazySingleton(classType: PatchOptionsViewModel),
|
||||||
LazySingleton(classType: NavigationService),
|
LazySingleton(classType: NavigationService),
|
||||||
LazySingleton(classType: ManagerAPI),
|
LazySingleton(classType: ManagerAPI),
|
||||||
LazySingleton(classType: PatcherAPI),
|
LazySingleton(classType: PatcherAPI),
|
||||||
|
@ -9,6 +9,7 @@ class Patch {
|
|||||||
required this.description,
|
required this.description,
|
||||||
required this.excluded,
|
required this.excluded,
|
||||||
required this.compatiblePackages,
|
required this.compatiblePackages,
|
||||||
|
required this.options,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory Patch.fromJson(Map<String, dynamic> json) => _$PatchFromJson(json);
|
factory Patch.fromJson(Map<String, dynamic> json) => _$PatchFromJson(json);
|
||||||
@ -16,6 +17,7 @@ class Patch {
|
|||||||
final String? description;
|
final String? description;
|
||||||
final bool excluded;
|
final bool excluded;
|
||||||
final List<Package> compatiblePackages;
|
final List<Package> compatiblePackages;
|
||||||
|
final List<Option> options;
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => _$PatchToJson(this);
|
Map<String, dynamic> toJson() => _$PatchToJson(this);
|
||||||
|
|
||||||
@ -33,8 +35,32 @@ class Package {
|
|||||||
|
|
||||||
factory Package.fromJson(Map<String, dynamic> json) =>
|
factory Package.fromJson(Map<String, dynamic> json) =>
|
||||||
_$PackageFromJson(json);
|
_$PackageFromJson(json);
|
||||||
|
|
||||||
final String name;
|
final String name;
|
||||||
final List<String> versions;
|
final List<String> versions;
|
||||||
|
|
||||||
Map toJson() => _$PackageToJson(this);
|
Map toJson() => _$PackageToJson(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class Option {
|
||||||
|
Option({
|
||||||
|
required this.key,
|
||||||
|
required this.title,
|
||||||
|
required this.description,
|
||||||
|
required this.value,
|
||||||
|
required this.required,
|
||||||
|
required this.optionClassType,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory Option.fromJson(Map<String, dynamic> json) => _$OptionFromJson(json);
|
||||||
|
|
||||||
|
final String key;
|
||||||
|
final String title;
|
||||||
|
final String description;
|
||||||
|
dynamic value;
|
||||||
|
final bool required;
|
||||||
|
final String optionClassType;
|
||||||
|
|
||||||
|
Map toJson() => _$OptionToJson(this);
|
||||||
|
}
|
||||||
|
@ -28,6 +28,10 @@ class ManagerAPI {
|
|||||||
final String cliRepo = 'revanced-cli';
|
final String cliRepo = 'revanced-cli';
|
||||||
late SharedPreferences _prefs;
|
late SharedPreferences _prefs;
|
||||||
List<Patch> patches = [];
|
List<Patch> patches = [];
|
||||||
|
List<Option> modifiedOptions = [];
|
||||||
|
List<Option> options = [];
|
||||||
|
Patch? selectedPatch;
|
||||||
|
BuildContext? ctx;
|
||||||
bool isRooted = false;
|
bool isRooted = false;
|
||||||
String storedPatchesFile = '/selected-patches.json';
|
String storedPatchesFile = '/selected-patches.json';
|
||||||
String keystoreFile =
|
String keystoreFile =
|
||||||
@ -182,6 +186,29 @@ class ManagerAPI {
|
|||||||
await _prefs.setStringList('usedPatches-$packageName', patchesJson);
|
await _prefs.setStringList('usedPatches-$packageName', patchesJson);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Option? getPatchOption(String packageName, String patchName, String key) {
|
||||||
|
final String? optionJson =
|
||||||
|
_prefs.getString('patchOption-$packageName-$patchName-$key');
|
||||||
|
if (optionJson != null) {
|
||||||
|
final Option option = Option.fromJson(jsonDecode(optionJson));
|
||||||
|
return option;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void setPatchOption(Option option, String patchName, String packageName) {
|
||||||
|
final String optionJson = jsonEncode(option.toJson());
|
||||||
|
_prefs.setString(
|
||||||
|
'patchOption-$packageName-$patchName-${option.key}',
|
||||||
|
optionJson,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearPatchOption(String packageName, String patchName, String key) {
|
||||||
|
_prefs.remove('patchOption-$packageName-$patchName-$key');
|
||||||
|
}
|
||||||
|
|
||||||
String getIntegrationsRepo() {
|
String getIntegrationsRepo() {
|
||||||
return _prefs.getString('integrationsRepo') ?? defaultIntegrationsRepo;
|
return _prefs.getString('integrationsRepo') ?? defaultIntegrationsRepo;
|
||||||
}
|
}
|
||||||
@ -314,7 +341,6 @@ class ManagerAPI {
|
|||||||
Directory('${appCache.path}/cache').createTempSync('tmp-');
|
Directory('${appCache.path}/cache').createTempSync('tmp-');
|
||||||
final Directory cacheDir = Directory('${workDir.path}/cache');
|
final Directory cacheDir = Directory('${workDir.path}/cache');
|
||||||
cacheDir.createSync();
|
cacheDir.createSync();
|
||||||
|
|
||||||
if (patchBundleFile != null) {
|
if (patchBundleFile != null) {
|
||||||
try {
|
try {
|
||||||
final String patchesJson = await PatcherAPI.patcherChannel.invokeMethod(
|
final String patchesJson = await PatcherAPI.patcherChannel.invokeMethod(
|
||||||
@ -324,7 +350,6 @@ class ManagerAPI {
|
|||||||
'cacheDirPath': cacheDir.path,
|
'cacheDirPath': cacheDir.path,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
final List<dynamic> patchesJsonList = jsonDecode(patchesJson);
|
final List<dynamic> patchesJsonList = jsonDecode(patchesJson);
|
||||||
patches = patchesJsonList
|
patches = patchesJsonList
|
||||||
.map((patchJson) => Patch.fromJson(patchJson))
|
.map((patchJson) => Patch.fromJson(patchJson))
|
||||||
@ -336,6 +361,7 @@ class ManagerAPI {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return List.empty();
|
return List.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -688,6 +714,14 @@ class ManagerAPI {
|
|||||||
return jsonDecode(string);
|
return jsonDecode(string);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void resetAllOptions() {
|
||||||
|
_prefs.getKeys().where((key) => key.startsWith('patchOption-')).forEach(
|
||||||
|
(key) {
|
||||||
|
_prefs.remove(key);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> resetLastSelectedPatches() async {
|
Future<void> resetLastSelectedPatches() async {
|
||||||
final File selectedPatchesFile = File(storedPatchesFile);
|
final File selectedPatchesFile = File(storedPatchesFile);
|
||||||
if (selectedPatchesFile.existsSync()) {
|
if (selectedPatchesFile.existsSync()) {
|
||||||
|
@ -79,8 +79,7 @@ class PatcherAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<List<ApplicationWithIcon>> getFilteredInstalledApps(
|
Future<List<ApplicationWithIcon>> getFilteredInstalledApps(
|
||||||
bool showUniversalPatches,
|
bool showUniversalPatches,) async {
|
||||||
) async {
|
|
||||||
final List<ApplicationWithIcon> filteredApps = [];
|
final List<ApplicationWithIcon> filteredApps = [];
|
||||||
final bool allAppsIncluded =
|
final bool allAppsIncluded =
|
||||||
_universalPatches.isNotEmpty && showUniversalPatches;
|
_universalPatches.isNotEmpty && showUniversalPatches;
|
||||||
@ -138,20 +137,29 @@ class PatcherAPI {
|
|||||||
return filteredPatches[packageName];
|
return filteredPatches[packageName];
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<Patch>> getAppliedPatches(
|
Future<List<Patch>> getAppliedPatches(List<String> appliedPatches,) async {
|
||||||
List<String> appliedPatches,
|
|
||||||
) async {
|
|
||||||
return _patches
|
return _patches
|
||||||
.where((patch) => appliedPatches.contains(patch.name))
|
.where((patch) => appliedPatches.contains(patch.name))
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> runPatcher(
|
Future<void> runPatcher(String packageName,
|
||||||
String packageName,
|
|
||||||
String apkFilePath,
|
String apkFilePath,
|
||||||
List<Patch> selectedPatches,
|
List<Patch> selectedPatches,) async {
|
||||||
) async {
|
|
||||||
final File? integrationsFile = await _managerAPI.downloadIntegrations();
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (integrationsFile != null) {
|
if (integrationsFile != null) {
|
||||||
_dataDir.createSync();
|
_dataDir.createSync();
|
||||||
@ -163,6 +171,7 @@ class PatcherAPI {
|
|||||||
final Directory cacheDir = Directory('${workDir.path}/cache');
|
final Directory cacheDir = Directory('${workDir.path}/cache');
|
||||||
cacheDir.createSync();
|
cacheDir.createSync();
|
||||||
final String originalFilePath = apkFilePath;
|
final String originalFilePath = apkFilePath;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await patcherChannel.invokeMethod(
|
await patcherChannel.invokeMethod(
|
||||||
'runPatcher',
|
'runPatcher',
|
||||||
@ -173,6 +182,7 @@ class PatcherAPI {
|
|||||||
'outFilePath': outFile!.path,
|
'outFilePath': outFile!.path,
|
||||||
'integrationsPath': integrationsFile.path,
|
'integrationsPath': integrationsFile.path,
|
||||||
'selectedPatches': selectedPatches.map((p) => p.name).toList(),
|
'selectedPatches': selectedPatches.map((p) => p.name).toList(),
|
||||||
|
'options': options,
|
||||||
'cacheDirPath': cacheDir.path,
|
'cacheDirPath': cacheDir.path,
|
||||||
'keyStoreFilePath': _keyStoreFile.path,
|
'keyStoreFilePath': _keyStoreFile.path,
|
||||||
'keystorePassword': _managerAPI.getKeystorePassword(),
|
'keystorePassword': _managerAPI.getKeystorePassword(),
|
||||||
@ -184,9 +194,9 @@ class PatcherAPI {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> stopPatcher() async {
|
Future<void> stopPatcher() async {
|
||||||
try {
|
try {
|
||||||
await patcherChannel.invokeMethod('stopPatcher');
|
await patcherChannel.invokeMethod('stopPatcher');
|
||||||
} on Exception catch (e) {
|
} on Exception catch (e) {
|
||||||
@ -194,9 +204,9 @@ class PatcherAPI {
|
|||||||
print(e);
|
print(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> installPatchedFile(PatchedApplication patchedApp) async {
|
Future<bool> installPatchedFile(PatchedApplication patchedApp) async {
|
||||||
if (outFile != null) {
|
if (outFile != null) {
|
||||||
try {
|
try {
|
||||||
if (patchedApp.isRooted) {
|
if (patchedApp.isRooted) {
|
||||||
@ -220,9 +230,9 @@ class PatcherAPI {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void exportPatchedFile(String appName, String version) {
|
void exportPatchedFile(String appName, String version) {
|
||||||
try {
|
try {
|
||||||
if (outFile != null) {
|
if (outFile != null) {
|
||||||
final String newName = _getFileName(appName, version);
|
final String newName = _getFileName(appName, version);
|
||||||
@ -238,9 +248,9 @@ class PatcherAPI {
|
|||||||
print(e);
|
print(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void sharePatchedFile(String appName, String version) {
|
void sharePatchedFile(String appName, String version) {
|
||||||
try {
|
try {
|
||||||
if (outFile != null) {
|
if (outFile != null) {
|
||||||
final String newName = _getFileName(appName, version);
|
final String newName = _getFileName(appName, version);
|
||||||
@ -255,15 +265,15 @@ class PatcherAPI {
|
|||||||
print(e);
|
print(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _getFileName(String appName, String version) {
|
String _getFileName(String appName, String version) {
|
||||||
final String prefix = appName.toLowerCase().replaceAll(' ', '-');
|
final String prefix = appName.toLowerCase().replaceAll(' ', '-');
|
||||||
final String newName = '$prefix-revanced_v$version.apk';
|
final String newName = '$prefix-revanced_v$version.apk';
|
||||||
return newName;
|
return newName;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> exportPatcherLog(String logs) async {
|
Future<void> exportPatcherLog(String logs) async {
|
||||||
final Directory appCache = await getTemporaryDirectory();
|
final Directory appCache = await getTemporaryDirectory();
|
||||||
final Directory logDir = Directory('${appCache.path}/logs');
|
final Directory logDir = Directory('${appCache.path}/logs');
|
||||||
logDir.createSync();
|
logDir.createSync();
|
||||||
@ -282,9 +292,9 @@ class PatcherAPI {
|
|||||||
destinationFileName: fileName,
|
destinationFileName: fileName,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
String getSuggestedVersion(String packageName) {
|
String getSuggestedVersion(String packageName) {
|
||||||
final Map<String, int> versions = {};
|
final Map<String, int> versions = {};
|
||||||
for (final Patch patch in _patches) {
|
for (final Patch patch in _patches) {
|
||||||
final Package? package = patch.compatiblePackages.firstWhereOrNull(
|
final Package? package = patch.compatiblePackages.firstWhereOrNull(
|
||||||
@ -307,8 +317,8 @@ class PatcherAPI {
|
|||||||
..clear()
|
..clear()
|
||||||
..addEntries(entries);
|
..addEntries(entries);
|
||||||
versions.removeWhere((key, value) => value != versions.values.last);
|
versions.removeWhere((key, value) => value != versions.values.last);
|
||||||
return (versions.keys.toList()..sort()).last;
|
return (versions.keys.toList()
|
||||||
|
..sort()).last;
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
}
|
}}
|
||||||
}
|
|
||||||
|
@ -23,7 +23,6 @@ class _AppSelectorViewState extends State<AppSelectorView> {
|
|||||||
onViewModelReady: (model) => model.initialize(),
|
onViewModelReady: (model) => model.initialize(),
|
||||||
viewModelBuilder: () => AppSelectorViewModel(),
|
viewModelBuilder: () => AppSelectorViewModel(),
|
||||||
builder: (context, model, child) => Scaffold(
|
builder: (context, model, child) => Scaffold(
|
||||||
resizeToAvoidBottomInset: false,
|
|
||||||
floatingActionButton: FloatingActionButton.extended(
|
floatingActionButton: FloatingActionButton.extended(
|
||||||
label: I18nText('appSelectorView.storageButton'),
|
label: I18nText('appSelectorView.storageButton'),
|
||||||
icon: const Icon(Icons.sd_storage),
|
icon: const Icon(Icons.sd_storage),
|
||||||
|
@ -13,6 +13,7 @@ import 'package:revanced_manager/services/patcher_api.dart';
|
|||||||
import 'package:revanced_manager/services/toast.dart';
|
import 'package:revanced_manager/services/toast.dart';
|
||||||
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
|
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
|
||||||
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
|
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
|
||||||
|
import 'package:revanced_manager/utils/check_for_supported_patch.dart';
|
||||||
import 'package:stacked/stacked.dart';
|
import 'package:stacked/stacked.dart';
|
||||||
|
|
||||||
class AppSelectorViewModel extends BaseViewModel {
|
class AppSelectorViewModel extends BaseViewModel {
|
||||||
@ -78,7 +79,7 @@ class AppSelectorViewModel extends BaseViewModel {
|
|||||||
icon: application.icon,
|
icon: application.icon,
|
||||||
patchDate: DateTime.now(),
|
patchDate: DateTime.now(),
|
||||||
);
|
);
|
||||||
locator<PatcherViewModel>().loadLastSelectedPatches();
|
await locator<PatcherViewModel>().loadLastSelectedPatches();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> canSelectInstalled(
|
Future<void> canSelectInstalled(
|
||||||
@ -93,10 +94,14 @@ class AppSelectorViewModel extends BaseViewModel {
|
|||||||
return showSelectFromStorageDialog(context);
|
return showSelectFromStorageDialog(context);
|
||||||
}
|
}
|
||||||
} else if (!await checkSplitApk(packageName) || isRooted) {
|
} else if (!await checkSplitApk(packageName) || isRooted) {
|
||||||
selectApp(app);
|
await selectApp(app);
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
}
|
}
|
||||||
|
final List<Option> requiredNullOptions = getNullRequiredOptions(locator<PatcherViewModel>().selectedPatches, packageName);
|
||||||
|
if(requiredNullOptions.isNotEmpty){
|
||||||
|
locator<PatcherViewModel>().showRequiredOptionDialog();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,6 @@ class HomeViewModel extends BaseViewModel {
|
|||||||
final RevancedAPI _revancedAPI = locator<RevancedAPI>();
|
final RevancedAPI _revancedAPI = locator<RevancedAPI>();
|
||||||
final Toast _toast = locator<Toast>();
|
final Toast _toast = locator<Toast>();
|
||||||
final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
|
final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
|
||||||
DateTime? _lastUpdate;
|
|
||||||
bool showUpdatableApps = false;
|
bool showUpdatableApps = false;
|
||||||
List<PatchedApplication> patchedInstalledApps = [];
|
List<PatchedApplication> patchedInstalledApps = [];
|
||||||
String? _latestManagerVersion = '';
|
String? _latestManagerVersion = '';
|
||||||
|
@ -139,6 +139,7 @@ class InstallerViewModel extends BaseViewModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> runPatcher() async {
|
Future<void> runPatcher() async {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await _patcherAPI.runPatcher(
|
await _patcherAPI.runPatcher(
|
||||||
_app.packageName,
|
_app.packageName,
|
||||||
@ -156,8 +157,8 @@ class InstallerViewModel extends BaseViewModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Necessary to reset the state of patches by reloading them
|
// Necessary to reset the state of patches so that they
|
||||||
// in a later patching process.
|
// can be reloaded again.
|
||||||
_managerAPI.patches.clear();
|
_managerAPI.patches.clear();
|
||||||
await _patcherAPI.loadPatches();
|
await _patcherAPI.loadPatches();
|
||||||
|
|
||||||
|
129
lib/ui/views/patch_options/patch_options_view.dart
Normal file
129
lib/ui/views/patch_options/patch_options_view.dart
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_i18n/flutter_i18n.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
import 'package:revanced_manager/models/patch.dart';
|
||||||
|
import 'package:revanced_manager/ui/views/patch_options/patch_options_viewmodel.dart';
|
||||||
|
import 'package:revanced_manager/ui/widgets/patchesSelectorView/patch_options_fields.dart';
|
||||||
|
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
|
||||||
|
import 'package:stacked/stacked.dart';
|
||||||
|
|
||||||
|
class PatchOptionsView extends StatelessWidget {
|
||||||
|
const PatchOptionsView({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ViewModelBuilder<PatchOptionsViewModel>.reactive(
|
||||||
|
onViewModelReady: (model) => model.initialize(),
|
||||||
|
viewModelBuilder: () => PatchOptionsViewModel(),
|
||||||
|
builder: (context, model, child) => GestureDetector(
|
||||||
|
onTap: () => FocusScope.of(context).unfocus(),
|
||||||
|
child: Scaffold(
|
||||||
|
floatingActionButton: FloatingActionButton.extended(
|
||||||
|
onPressed: () async {
|
||||||
|
final bool saved = model.saveOptions(context);
|
||||||
|
if (saved && context.mounted) {
|
||||||
|
Navigator.pop(context);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
label: I18nText('patchOptionsView.saveOptions'),
|
||||||
|
icon: const Icon(Icons.save),
|
||||||
|
),
|
||||||
|
body: CustomScrollView(
|
||||||
|
slivers: <Widget>[
|
||||||
|
SliverAppBar(
|
||||||
|
title: I18nText(
|
||||||
|
'patchOptionsView.viewTitle',
|
||||||
|
child: Text(
|
||||||
|
'',
|
||||||
|
style: GoogleFonts.inter(
|
||||||
|
color: Theme.of(context).textTheme.titleLarge!.color,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
model.resetOptions();
|
||||||
|
},
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.history,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
for (final Option option in model.visibleOptions)
|
||||||
|
if (option.optionClassType == 'StringPatchOption' ||
|
||||||
|
option.optionClassType == 'IntPatchOption')
|
||||||
|
IntAndStringPatchOption(
|
||||||
|
patchOption: option,
|
||||||
|
removeOption: (option) {
|
||||||
|
model.removeOption(option);
|
||||||
|
},
|
||||||
|
onChanged: (value, option) {
|
||||||
|
model.modifyOptions(value, option);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
else if (option.optionClassType == 'BooleanPatchOption')
|
||||||
|
BooleanPatchOption(
|
||||||
|
patchOption: option,
|
||||||
|
removeOption: (option) {
|
||||||
|
model.removeOption(option);
|
||||||
|
},
|
||||||
|
onChanged: (value, option) {
|
||||||
|
model.modifyOptions(value, option);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
else if (option.optionClassType ==
|
||||||
|
'StringListPatchOption' ||
|
||||||
|
option.optionClassType == 'IntListPatchOption' ||
|
||||||
|
option.optionClassType == 'LongListPatchOption')
|
||||||
|
IntStringLongListPatchOption(
|
||||||
|
patchOption: option,
|
||||||
|
removeOption: (option) {
|
||||||
|
model.removeOption(option);
|
||||||
|
},
|
||||||
|
onChanged: (value, option) {
|
||||||
|
model.modifyOptions(value, option);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
else
|
||||||
|
UnsupportedPatchOption(
|
||||||
|
patchOption: option,
|
||||||
|
),
|
||||||
|
if (model.visibleOptions.length !=
|
||||||
|
model.options.length) ...[
|
||||||
|
const SizedBox(
|
||||||
|
height: 8,
|
||||||
|
),
|
||||||
|
CustomMaterialButton(
|
||||||
|
onPressed: () {
|
||||||
|
model.showAddOptionDialog(context);
|
||||||
|
},
|
||||||
|
label: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.add),
|
||||||
|
I18nText('patchOptionsView.addOptions'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
const SizedBox(
|
||||||
|
height: 80,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
258
lib/ui/views/patch_options/patch_options_viewmodel.dart
Normal file
258
lib/ui/views/patch_options/patch_options_viewmodel.dart
Normal file
@ -0,0 +1,258 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_i18n/widgets/I18nText.dart';
|
||||||
|
import 'package:revanced_manager/app/app.locator.dart';
|
||||||
|
import 'package:revanced_manager/models/patch.dart';
|
||||||
|
import 'package:revanced_manager/services/manager_api.dart';
|
||||||
|
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
|
||||||
|
import 'package:revanced_manager/ui/views/patches_selector/patches_selector_viewmodel.dart';
|
||||||
|
import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
|
||||||
|
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
|
||||||
|
import 'package:stacked/stacked.dart';
|
||||||
|
|
||||||
|
class PatchOptionsViewModel extends BaseViewModel {
|
||||||
|
final ManagerAPI _managerAPI = locator<ManagerAPI>();
|
||||||
|
final String selectedApp =
|
||||||
|
locator<PatcherViewModel>().selectedApp!.packageName;
|
||||||
|
List<Option> options = [];
|
||||||
|
List<Option> savedOptions = [];
|
||||||
|
List<Option> visibleOptions = [];
|
||||||
|
|
||||||
|
Future<void> initialize() async {
|
||||||
|
options = getDefaultOptions();
|
||||||
|
for (final Option option in options) {
|
||||||
|
final Option? savedOption = _managerAPI.getPatchOption(
|
||||||
|
selectedApp,
|
||||||
|
_managerAPI.selectedPatch!.name,
|
||||||
|
option.key,
|
||||||
|
);
|
||||||
|
if (savedOption != null) {
|
||||||
|
savedOptions.add(savedOption);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (savedOptions.isNotEmpty) {
|
||||||
|
visibleOptions = [
|
||||||
|
...savedOptions,
|
||||||
|
...options
|
||||||
|
.where(
|
||||||
|
(option) =>
|
||||||
|
option.required &&
|
||||||
|
!savedOptions.any((sOption) => sOption.key == option.key),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
visibleOptions = [
|
||||||
|
...options.where((option) => option.required).toList(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void addOption(Option option) {
|
||||||
|
visibleOptions.add(option);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void removeOption(Option option) {
|
||||||
|
visibleOptions.removeWhere((vOption) => vOption.key == option.key);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool saveOptions(BuildContext context) {
|
||||||
|
final List<Option> requiredNullOptions = [];
|
||||||
|
for (final Option option in options) {
|
||||||
|
if (!visibleOptions.any((vOption) => vOption.key == option.key)) {
|
||||||
|
_managerAPI.clearPatchOption(
|
||||||
|
selectedApp, _managerAPI.selectedPatch!.name, option.key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (final Option option in visibleOptions) {
|
||||||
|
if (option.required && option.value == null) {
|
||||||
|
requiredNullOptions.add(option);
|
||||||
|
} else {
|
||||||
|
_managerAPI.setPatchOption(
|
||||||
|
option, _managerAPI.selectedPatch!.name, selectedApp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (requiredNullOptions.isNotEmpty) {
|
||||||
|
showRequiredOptionNullDialog(
|
||||||
|
context,
|
||||||
|
requiredNullOptions,
|
||||||
|
_managerAPI,
|
||||||
|
selectedApp,
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void modifyOptions(dynamic value, Option option) {
|
||||||
|
final Option modifiedOption = Option(
|
||||||
|
title: option.title,
|
||||||
|
description: option.description,
|
||||||
|
optionClassType: option.optionClassType,
|
||||||
|
value: value,
|
||||||
|
required: option.required,
|
||||||
|
key: option.key,
|
||||||
|
);
|
||||||
|
visibleOptions[visibleOptions
|
||||||
|
.indexWhere((vOption) => vOption.key == option.key)] = modifiedOption;
|
||||||
|
_managerAPI.modifiedOptions
|
||||||
|
.removeWhere((mOption) => mOption.key == option.key);
|
||||||
|
_managerAPI.modifiedOptions.add(modifiedOption);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Option> getDefaultOptions() {
|
||||||
|
final List<Option> defaultOptions = [];
|
||||||
|
for (final option in _managerAPI.options) {
|
||||||
|
final Option defaultOption = Option(
|
||||||
|
title: option.title,
|
||||||
|
description: option.description,
|
||||||
|
optionClassType: option.optionClassType,
|
||||||
|
value: option.value is List ? option.value.toList() : option.value,
|
||||||
|
required: option.required,
|
||||||
|
key: option.key,
|
||||||
|
);
|
||||||
|
defaultOptions.add(defaultOption);
|
||||||
|
}
|
||||||
|
return defaultOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
void resetOptions() {
|
||||||
|
_managerAPI.modifiedOptions.clear();
|
||||||
|
visibleOptions =
|
||||||
|
getDefaultOptions().where((option) => option.required).toList();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> showAddOptionDialog(BuildContext context) async {
|
||||||
|
await showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||||
|
title: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
I18nText(
|
||||||
|
'patchOptionsView.addOptions',
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
color: Theme.of(context).colorScheme.onSecondaryContainer,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
CustomMaterialButton(
|
||||||
|
label: I18nText('okButton'),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
contentPadding: const EdgeInsets.all(8),
|
||||||
|
content: Wrap(
|
||||||
|
spacing: 14,
|
||||||
|
runSpacing: 14,
|
||||||
|
children: options
|
||||||
|
.where(
|
||||||
|
(option) =>
|
||||||
|
!visibleOptions.any((vOption) => vOption.key == option.key),
|
||||||
|
)
|
||||||
|
.map((e) {
|
||||||
|
return CustomCard(
|
||||||
|
padding: const EdgeInsets.all(4),
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||||
|
onTap: () {
|
||||||
|
addOption(e);
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
e.title,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
e.description,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: Theme.of(context).colorScheme.onSecondaryContainer,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> showRequiredOptionNullDialog(
|
||||||
|
BuildContext context,
|
||||||
|
List<Option> options,
|
||||||
|
ManagerAPI managerAPI,
|
||||||
|
String selectedApp,
|
||||||
|
) async {
|
||||||
|
final List<String> optionsTitles = [];
|
||||||
|
for (final option in options) {
|
||||||
|
optionsTitles.add('• ${option.title}');
|
||||||
|
}
|
||||||
|
await showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||||
|
title: I18nText('notice'),
|
||||||
|
actions: [
|
||||||
|
CustomMaterialButton(
|
||||||
|
isFilled: false,
|
||||||
|
label: I18nText(
|
||||||
|
'patchOptionsView.deselectPatch',
|
||||||
|
),
|
||||||
|
onPressed: () async {
|
||||||
|
if (managerAPI.isPatchesChangeEnabled()) {
|
||||||
|
locator<PatcherViewModel>()
|
||||||
|
.selectedPatches
|
||||||
|
.remove(managerAPI.selectedPatch);
|
||||||
|
locator<PatcherViewModel>().notifyListeners();
|
||||||
|
for (final option in options) {
|
||||||
|
managerAPI.clearPatchOption(
|
||||||
|
selectedApp, managerAPI.selectedPatch!.name, option.key);
|
||||||
|
}
|
||||||
|
Navigator.of(context)
|
||||||
|
..pop()
|
||||||
|
..pop()
|
||||||
|
..pop();
|
||||||
|
} else {
|
||||||
|
PatchesSelectorViewModel().showPatchesChangeDialog(context);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
CustomMaterialButton(
|
||||||
|
label: I18nText('okButton'),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
content: I18nText(
|
||||||
|
'patchOptionsView.requiredOptionNull',
|
||||||
|
translationParams: {
|
||||||
|
'options': optionsTitles.join('\n'),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
@ -22,7 +22,14 @@ class PatcherView extends StatelessWidget {
|
|||||||
child: FloatingActionButton.extended(
|
child: FloatingActionButton.extended(
|
||||||
label: I18nText('patcherView.patchButton'),
|
label: I18nText('patcherView.patchButton'),
|
||||||
icon: const Icon(Icons.build),
|
icon: const Icon(Icons.build),
|
||||||
onPressed: () => model.showRemovedPatchesDialog(context),
|
onPressed: () async{
|
||||||
|
if (model.checkRequiredPatchOption(context)) {
|
||||||
|
final bool proceed = model.showRemovedPatchesDialog(context);
|
||||||
|
if (proceed && context.mounted) {
|
||||||
|
model.showArmv7WarningDialog(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
body: CustomScrollView(
|
body: CustomScrollView(
|
||||||
@ -45,7 +52,10 @@ class PatcherView extends StatelessWidget {
|
|||||||
delegate: SliverChildListDelegate.fixed(
|
delegate: SliverChildListDelegate.fixed(
|
||||||
<Widget>[
|
<Widget>[
|
||||||
AppSelectorCard(
|
AppSelectorCard(
|
||||||
onPressed: () => model.navigateToAppSelector(),
|
onPressed: () => {
|
||||||
|
model.navigateToAppSelector(),
|
||||||
|
model.ctx = context,
|
||||||
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Opacity(
|
Opacity(
|
||||||
|
@ -21,6 +21,7 @@ class PatcherViewModel extends BaseViewModel {
|
|||||||
final ManagerAPI _managerAPI = locator<ManagerAPI>();
|
final ManagerAPI _managerAPI = locator<ManagerAPI>();
|
||||||
final PatcherAPI _patcherAPI = locator<PatcherAPI>();
|
final PatcherAPI _patcherAPI = locator<PatcherAPI>();
|
||||||
PatchedApplication? selectedApp;
|
PatchedApplication? selectedApp;
|
||||||
|
BuildContext? ctx;
|
||||||
List<Patch> selectedPatches = [];
|
List<Patch> selectedPatches = [];
|
||||||
List<String> removedPatches = [];
|
List<String> removedPatches = [];
|
||||||
|
|
||||||
@ -44,9 +45,9 @@ class PatcherViewModel extends BaseViewModel {
|
|||||||
return selectedApp == null;
|
return selectedApp == null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> showRemovedPatchesDialog(BuildContext context) async {
|
bool showRemovedPatchesDialog(BuildContext context) {
|
||||||
if (removedPatches.isNotEmpty) {
|
if (removedPatches.isNotEmpty) {
|
||||||
return showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
title: I18nText('notice'),
|
title: I18nText('notice'),
|
||||||
@ -59,21 +60,58 @@ class PatcherViewModel extends BaseViewModel {
|
|||||||
CustomMaterialButton(
|
CustomMaterialButton(
|
||||||
isFilled: false,
|
isFilled: false,
|
||||||
label: I18nText('noButton'),
|
label: I18nText('noButton'),
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
),
|
),
|
||||||
CustomMaterialButton(
|
CustomMaterialButton(
|
||||||
label: I18nText('yesButton'),
|
label: I18nText('yesButton'),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
navigateToInstaller();
|
showArmv7WarningDialog(context);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
return false;
|
||||||
showArmv7WarningDialog(context); // TODO(aabed): Find out why this is here
|
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool checkRequiredPatchOption(BuildContext context) {
|
||||||
|
if (getNullRequiredOptions(selectedPatches, selectedApp!.packageName).isNotEmpty) {
|
||||||
|
showRequiredOptionDialog(context);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void showRequiredOptionDialog([context]) {
|
||||||
|
showDialog(
|
||||||
|
context: context ?? ctx,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: I18nText('notice'),
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||||
|
content: I18nText('patcherView.requiredOptionDialogText'),
|
||||||
|
actions: <Widget>[
|
||||||
|
CustomMaterialButton(
|
||||||
|
isFilled: false,
|
||||||
|
label: I18nText('cancelButton'),
|
||||||
|
onPressed: () => {
|
||||||
|
Navigator.of(context).pop(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
CustomMaterialButton(
|
||||||
|
label: I18nText('okButton'),
|
||||||
|
onPressed: () => {
|
||||||
|
Navigator.pop(context),
|
||||||
|
navigateToPatchesSelector(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> showArmv7WarningDialog(BuildContext context) async {
|
Future<void> showArmv7WarningDialog(BuildContext context) async {
|
||||||
@ -163,7 +201,10 @@ class PatcherViewModel extends BaseViewModel {
|
|||||||
final usedPatches = _managerAPI.getUsedPatches(selectedApp!.packageName);
|
final usedPatches = _managerAPI.getUsedPatches(selectedApp!.packageName);
|
||||||
for (final patch in usedPatches){
|
for (final patch in usedPatches){
|
||||||
if (!patches.any((p) => p.name == patch.name)){
|
if (!patches.any((p) => p.name == patch.name)){
|
||||||
removedPatches.add('\u2022 ${patch.name}');
|
removedPatches.add('• ${patch.name}');
|
||||||
|
for (final option in patch.options) {
|
||||||
|
_managerAPI.clearPatchOption(selectedApp!.packageName, patch.name, option.key);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
@ -37,7 +37,6 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
|
|||||||
onViewModelReady: (model) => model.initialize(),
|
onViewModelReady: (model) => model.initialize(),
|
||||||
viewModelBuilder: () => PatchesSelectorViewModel(),
|
viewModelBuilder: () => PatchesSelectorViewModel(),
|
||||||
builder: (context, model, child) => Scaffold(
|
builder: (context, model, child) => Scaffold(
|
||||||
resizeToAvoidBottomInset: false,
|
|
||||||
floatingActionButton: Visibility(
|
floatingActionButton: Visibility(
|
||||||
visible: model.patches.isNotEmpty,
|
visible: model.patches.isNotEmpty,
|
||||||
child: FloatingActionButton.extended(
|
child: FloatingActionButton.extended(
|
||||||
@ -49,8 +48,10 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
|
|||||||
),
|
),
|
||||||
icon: const Icon(Icons.check),
|
icon: const Icon(Icons.check),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
|
if (!model.areRequiredOptionsNull(context)) {
|
||||||
model.selectPatches();
|
model.selectPatches();
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -73,7 +74,10 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
|
|||||||
Icons.arrow_back,
|
Icons.arrow_back,
|
||||||
color: Theme.of(context).textTheme.titleLarge!.color,
|
color: Theme.of(context).textTheme.titleLarge!.color,
|
||||||
),
|
),
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
onPressed: () {
|
||||||
|
model.resetSelection();
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
FittedBox(
|
FittedBox(
|
||||||
@ -188,6 +192,93 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
if (model.newPatchExists())
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
vertical: 10.0,
|
||||||
|
),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
top: 10.0,
|
||||||
|
bottom: 10.0,
|
||||||
|
left: 5.0,
|
||||||
|
),
|
||||||
|
child: I18nText(
|
||||||
|
'patchesSelectorView.newPatches',
|
||||||
|
child: Text(
|
||||||
|
'',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
...model.getQueriedPatches(_query).map((patch) {
|
||||||
|
if (model.isPatchNew(patch)) {
|
||||||
|
return PatchItem(
|
||||||
|
name: patch.name,
|
||||||
|
simpleName: patch.getSimpleName(),
|
||||||
|
description: patch.description ?? '',
|
||||||
|
packageVersion:
|
||||||
|
model.getAppInfo().version,
|
||||||
|
supportedPackageVersions:
|
||||||
|
model.getSupportedVersions(patch),
|
||||||
|
isUnsupported: !isPatchSupported(patch),
|
||||||
|
isChangeEnabled:
|
||||||
|
_managerAPI.isPatchesChangeEnabled(),
|
||||||
|
hasUnsupportedPatchOption:
|
||||||
|
hasUnsupportedRequiredOption(
|
||||||
|
patch.options,
|
||||||
|
patch,
|
||||||
|
),
|
||||||
|
options: patch.options,
|
||||||
|
isSelected: model.isSelected(patch),
|
||||||
|
navigateToOptions: (options) =>
|
||||||
|
model.navigateToPatchOptions(
|
||||||
|
options,
|
||||||
|
patch,
|
||||||
|
),
|
||||||
|
onChanged: (value) => model.selectPatch(
|
||||||
|
patch,
|
||||||
|
value,
|
||||||
|
context,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Container();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
vertical: 10.0,
|
||||||
|
),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
top: 10.0,
|
||||||
|
bottom: 10.0,
|
||||||
|
left: 5.0,
|
||||||
|
),
|
||||||
|
child: I18nText(
|
||||||
|
'patchesSelectorView.patches',
|
||||||
|
child: Text(
|
||||||
|
'',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
...model.getQueriedPatches(_query).map(
|
...model.getQueriedPatches(_query).map(
|
||||||
(patch) {
|
(patch) {
|
||||||
if (patch.compatiblePackages.isNotEmpty) {
|
if (patch.compatiblePackages.isNotEmpty) {
|
||||||
@ -201,13 +292,21 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
|
|||||||
isUnsupported: !isPatchSupported(patch),
|
isUnsupported: !isPatchSupported(patch),
|
||||||
isChangeEnabled:
|
isChangeEnabled:
|
||||||
_managerAPI.isPatchesChangeEnabled(),
|
_managerAPI.isPatchesChangeEnabled(),
|
||||||
isNew: model.isPatchNew(
|
hasUnsupportedPatchOption:
|
||||||
patch,
|
hasUnsupportedRequiredOption(
|
||||||
model.getAppInfo().packageName,
|
patch.options, patch),
|
||||||
),
|
options: patch.options,
|
||||||
isSelected: model.isSelected(patch),
|
isSelected: model.isSelected(patch),
|
||||||
onChanged: (value) =>
|
navigateToOptions: (options) =>
|
||||||
model.selectPatch(patch, value, context),
|
model.navigateToPatchOptions(
|
||||||
|
options,
|
||||||
|
patch,
|
||||||
|
),
|
||||||
|
onChanged: (value) => model.selectPatch(
|
||||||
|
patch,
|
||||||
|
value,
|
||||||
|
context,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return Container();
|
return Container();
|
||||||
@ -254,8 +353,18 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
|
|||||||
isUnsupported: !isPatchSupported(patch),
|
isUnsupported: !isPatchSupported(patch),
|
||||||
isChangeEnabled:
|
isChangeEnabled:
|
||||||
_managerAPI.isPatchesChangeEnabled(),
|
_managerAPI.isPatchesChangeEnabled(),
|
||||||
isNew: false,
|
hasUnsupportedPatchOption:
|
||||||
|
hasUnsupportedRequiredOption(
|
||||||
|
patch.options,
|
||||||
|
patch,
|
||||||
|
),
|
||||||
|
options: patch.options,
|
||||||
isSelected: model.isSelected(patch),
|
isSelected: model.isSelected(patch),
|
||||||
|
navigateToOptions: (options) =>
|
||||||
|
model.navigateToPatchOptions(
|
||||||
|
options,
|
||||||
|
patch,
|
||||||
|
),
|
||||||
onChanged: (value) => model.selectPatch(
|
onChanged: (value) => model.selectPatch(
|
||||||
patch,
|
patch,
|
||||||
value,
|
value,
|
||||||
|
@ -2,6 +2,7 @@ import 'package:collection/collection.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_i18n/widgets/I18nText.dart';
|
import 'package:flutter_i18n/widgets/I18nText.dart';
|
||||||
import 'package:revanced_manager/app/app.locator.dart';
|
import 'package:revanced_manager/app/app.locator.dart';
|
||||||
|
import 'package:revanced_manager/app/app.router.dart';
|
||||||
import 'package:revanced_manager/models/patch.dart';
|
import 'package:revanced_manager/models/patch.dart';
|
||||||
import 'package:revanced_manager/models/patched_application.dart';
|
import 'package:revanced_manager/models/patched_application.dart';
|
||||||
import 'package:revanced_manager/services/manager_api.dart';
|
import 'package:revanced_manager/services/manager_api.dart';
|
||||||
@ -11,11 +12,14 @@ import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
|
|||||||
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
|
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
|
||||||
import 'package:revanced_manager/utils/check_for_supported_patch.dart';
|
import 'package:revanced_manager/utils/check_for_supported_patch.dart';
|
||||||
import 'package:stacked/stacked.dart';
|
import 'package:stacked/stacked.dart';
|
||||||
|
import 'package:stacked_services/stacked_services.dart';
|
||||||
|
|
||||||
class PatchesSelectorViewModel extends BaseViewModel {
|
class PatchesSelectorViewModel extends BaseViewModel {
|
||||||
final PatcherAPI _patcherAPI = locator<PatcherAPI>();
|
final PatcherAPI _patcherAPI = locator<PatcherAPI>();
|
||||||
final ManagerAPI _managerAPI = locator<ManagerAPI>();
|
final ManagerAPI _managerAPI = locator<ManagerAPI>();
|
||||||
|
final NavigationService _navigationService = locator<NavigationService>();
|
||||||
final List<Patch> patches = [];
|
final List<Patch> patches = [];
|
||||||
|
final List<Patch> currentSelection = [];
|
||||||
final List<Patch> selectedPatches =
|
final List<Patch> selectedPatches =
|
||||||
locator<PatcherViewModel>().selectedPatches;
|
locator<PatcherViewModel>().selectedPatches;
|
||||||
PatchedApplication? selectedApp = locator<PatcherViewModel>().selectedApp;
|
PatchedApplication? selectedApp = locator<PatcherViewModel>().selectedApp;
|
||||||
@ -31,14 +35,18 @@ class PatchesSelectorViewModel extends BaseViewModel {
|
|||||||
selectedApp!.packageName,
|
selectedApp!.packageName,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
final List<Option> requiredNullOptions =
|
||||||
|
getNullRequiredOptions(patches, selectedApp!.packageName);
|
||||||
patches.sort((a, b) {
|
patches.sort((a, b) {
|
||||||
if (isPatchNew(a, selectedApp!.packageName) ==
|
if (b.options.any((option) => requiredNullOptions.contains(option)) &&
|
||||||
isPatchNew(b, selectedApp!.packageName)) {
|
a.options.isEmpty) {
|
||||||
return a.name.compareTo(b.name);
|
return 1;
|
||||||
} else {
|
} else {
|
||||||
return isPatchNew(b, selectedApp!.packageName) ? 1 : -1;
|
return a.name.compareTo(b.name);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
currentSelection.clear();
|
||||||
|
currentSelection.addAll(selectedPatches);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,6 +56,60 @@ class PatchesSelectorViewModel extends BaseViewModel {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void navigateToPatchOptions(List<Option> setOptions, Patch patch) {
|
||||||
|
_managerAPI.options = setOptions;
|
||||||
|
_managerAPI.selectedPatch = patch;
|
||||||
|
_managerAPI.modifiedOptions.clear();
|
||||||
|
_navigationService.navigateToPatchOptionsView();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool areRequiredOptionsNull(BuildContext context) {
|
||||||
|
final List<String> patchesWithNullRequiredOptions = [];
|
||||||
|
final List<Option> requiredNullOptions =
|
||||||
|
getNullRequiredOptions(selectedPatches, selectedApp!.packageName);
|
||||||
|
if (requiredNullOptions.isNotEmpty) {
|
||||||
|
for (final patch in selectedPatches) {
|
||||||
|
for (final patchOption in patch.options) {
|
||||||
|
if (requiredNullOptions.contains(patchOption)) {
|
||||||
|
patchesWithNullRequiredOptions.add(patch.name);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
showSetRequiredOption(context, patchesWithNullRequiredOptions);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> showSetRequiredOption(
|
||||||
|
BuildContext context,
|
||||||
|
List<String> patches,
|
||||||
|
) async {
|
||||||
|
return showDialog(
|
||||||
|
barrierDismissible: false,
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: I18nText('notice'),
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||||
|
content: I18nText(
|
||||||
|
'patchesSelectorView.setRequiredOption',
|
||||||
|
translationParams: {
|
||||||
|
'patches': patches.map((patch) => '• $patch').join('\n'),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
actions: <Widget>[
|
||||||
|
CustomMaterialButton(
|
||||||
|
label: I18nText('okButton'),
|
||||||
|
onPressed: () => {
|
||||||
|
Navigator.of(context).pop(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
void selectPatch(Patch patch, bool isSelected, BuildContext context) {
|
void selectPatch(Patch patch, bool isSelected, BuildContext context) {
|
||||||
if (_managerAPI.isPatchesChangeEnabled()) {
|
if (_managerAPI.isPatchesChangeEnabled()) {
|
||||||
if (isSelected && !selectedPatches.contains(patch)) {
|
if (isSelected && !selectedPatches.contains(patch)) {
|
||||||
@ -123,9 +185,19 @@ class PatchesSelectorViewModel extends BaseViewModel {
|
|||||||
void selectPatches() {
|
void selectPatches() {
|
||||||
locator<PatcherViewModel>().selectedPatches = selectedPatches;
|
locator<PatcherViewModel>().selectedPatches = selectedPatches;
|
||||||
saveSelectedPatches();
|
saveSelectedPatches();
|
||||||
|
if (_managerAPI.ctx != null) {
|
||||||
|
Navigator.pop(_managerAPI.ctx!);
|
||||||
|
_managerAPI.ctx = null;
|
||||||
|
}
|
||||||
locator<PatcherViewModel>().notifyListeners();
|
locator<PatcherViewModel>().notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void resetSelection() {
|
||||||
|
selectedPatches.clear();
|
||||||
|
selectedPatches.addAll(currentSelection);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> getPatchesVersion() async {
|
Future<void> getPatchesVersion() async {
|
||||||
patchesVersion = await _managerAPI.getCurrentPatchesVersion();
|
patchesVersion = await _managerAPI.getCurrentPatchesVersion();
|
||||||
}
|
}
|
||||||
@ -153,8 +225,9 @@ class PatchesSelectorViewModel extends BaseViewModel {
|
|||||||
return locator<PatcherViewModel>().selectedApp!;
|
return locator<PatcherViewModel>().selectedApp!;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isPatchNew(Patch patch, String packageName) {
|
bool isPatchNew(Patch patch) {
|
||||||
final List<Patch> savedPatches = _managerAPI.getSavedPatches(packageName);
|
final List<Patch> savedPatches =
|
||||||
|
_managerAPI.getSavedPatches(selectedApp!.packageName);
|
||||||
if (savedPatches.isEmpty) {
|
if (savedPatches.isEmpty) {
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
@ -163,6 +236,12 @@ class PatchesSelectorViewModel extends BaseViewModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool newPatchExists() {
|
||||||
|
return patches.any(
|
||||||
|
(patch) => isPatchNew(patch),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
List<String> getSupportedVersions(Patch patch) {
|
List<String> getSupportedVersions(Patch patch) {
|
||||||
final PatchedApplication app = locator<PatcherViewModel>().selectedApp!;
|
final PatchedApplication app = locator<PatcherViewModel>().selectedApp!;
|
||||||
final Package? package = patch.compatiblePackages.firstWhereOrNull(
|
final Package? package = patch.compatiblePackages.firstWhereOrNull(
|
||||||
|
@ -246,6 +246,11 @@ class SettingsViewModel extends BaseViewModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void resetAllOptions() {
|
||||||
|
_managerAPI.resetAllOptions();
|
||||||
|
_toast.showBottom('settingsView.resetStoredOptions');
|
||||||
|
}
|
||||||
|
|
||||||
void resetSelectedPatches() {
|
void resetSelectedPatches() {
|
||||||
_managerAPI.resetLastSelectedPatches();
|
_managerAPI.resetLastSelectedPatches();
|
||||||
_toast.showBottom('settingsView.resetStoredPatches');
|
_toast.showBottom('settingsView.resetStoredPatches');
|
||||||
|
@ -146,7 +146,7 @@ class AppInfoViewModel extends BaseViewModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String getAppliedPatchesString(List<String> appliedPatches) {
|
String getAppliedPatchesString(List<String> appliedPatches) {
|
||||||
return '\u2022 ${appliedPatches.join('\n\u2022 ')}';
|
return '• ${appliedPatches.join('\n• ')}';
|
||||||
}
|
}
|
||||||
|
|
||||||
void openApp(PatchedApplication app) {
|
void openApp(PatchedApplication app) {
|
||||||
|
@ -61,7 +61,7 @@ class PatchSelectorCard extends StatelessWidget {
|
|||||||
final List<Patch> selectedPatches = locator<PatcherViewModel>().selectedPatches;
|
final List<Patch> selectedPatches = locator<PatcherViewModel>().selectedPatches;
|
||||||
selectedPatches.sort((a, b) => a.name.compareTo(b.name));
|
selectedPatches.sort((a, b) => a.name.compareTo(b.name));
|
||||||
for (final Patch p in selectedPatches) {
|
for (final Patch p in selectedPatches) {
|
||||||
text += '\u2022 ${p.getSimpleName()}\n';
|
text += '• ${p.getSimpleName()}\n';
|
||||||
}
|
}
|
||||||
return text.substring(0, text.length - 1);
|
return text.substring(0, text.length - 1);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_i18n/flutter_i18n.dart';
|
import 'package:flutter_i18n/flutter_i18n.dart';
|
||||||
import 'package:revanced_manager/app/app.locator.dart';
|
import 'package:revanced_manager/app/app.locator.dart';
|
||||||
|
import 'package:revanced_manager/models/patch.dart';
|
||||||
import 'package:revanced_manager/services/manager_api.dart';
|
import 'package:revanced_manager/services/manager_api.dart';
|
||||||
import 'package:revanced_manager/services/toast.dart';
|
import 'package:revanced_manager/services/toast.dart';
|
||||||
import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
|
import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
|
||||||
@ -16,11 +17,12 @@ class PatchItem extends StatefulWidget {
|
|||||||
required this.packageVersion,
|
required this.packageVersion,
|
||||||
required this.supportedPackageVersions,
|
required this.supportedPackageVersions,
|
||||||
required this.isUnsupported,
|
required this.isUnsupported,
|
||||||
required this.isNew,
|
required this.hasUnsupportedPatchOption,
|
||||||
|
required this.options,
|
||||||
required this.isSelected,
|
required this.isSelected,
|
||||||
required this.onChanged,
|
required this.onChanged,
|
||||||
|
required this.navigateToOptions,
|
||||||
required this.isChangeEnabled,
|
required this.isChangeEnabled,
|
||||||
this.child,
|
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
final String name;
|
final String name;
|
||||||
final String simpleName;
|
final String simpleName;
|
||||||
@ -28,11 +30,12 @@ class PatchItem extends StatefulWidget {
|
|||||||
final String packageVersion;
|
final String packageVersion;
|
||||||
final List<String> supportedPackageVersions;
|
final List<String> supportedPackageVersions;
|
||||||
final bool isUnsupported;
|
final bool isUnsupported;
|
||||||
final bool isNew;
|
final bool hasUnsupportedPatchOption;
|
||||||
|
final List<Option> options;
|
||||||
bool isSelected;
|
bool isSelected;
|
||||||
final Function(bool) onChanged;
|
final Function(bool) onChanged;
|
||||||
|
final void Function(List<Option>) navigateToOptions;
|
||||||
final bool isChangeEnabled;
|
final bool isChangeEnabled;
|
||||||
final Widget? child;
|
|
||||||
final toast = locator<Toast>();
|
final toast = locator<Toast>();
|
||||||
final _managerAPI = locator<ManagerAPI>();
|
final _managerAPI = locator<ManagerAPI>();
|
||||||
|
|
||||||
@ -45,7 +48,8 @@ class _PatchItemState extends State<PatchItem> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
widget.isSelected = widget.isSelected &&
|
widget.isSelected = widget.isSelected &&
|
||||||
(!widget.isUnsupported ||
|
(!widget.isUnsupported ||
|
||||||
widget._managerAPI.areExperimentalPatchesEnabled());
|
widget._managerAPI.areExperimentalPatchesEnabled()) &&
|
||||||
|
!widget.hasUnsupportedPatchOption;
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
||||||
child: Opacity(
|
child: Opacity(
|
||||||
@ -54,34 +58,76 @@ class _PatchItemState extends State<PatchItem> {
|
|||||||
? 0.5
|
? 0.5
|
||||||
: 1,
|
: 1,
|
||||||
child: CustomCard(
|
child: CustomCard(
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
top: 12,
|
||||||
|
bottom: 16,
|
||||||
|
left: 8.0,
|
||||||
|
right: widget.options.isNotEmpty ? 4.0 : 8.0,
|
||||||
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
setState(() {
|
|
||||||
if (widget.isUnsupported &&
|
if (widget.isUnsupported &&
|
||||||
!widget._managerAPI.areExperimentalPatchesEnabled()) {
|
!widget._managerAPI.areExperimentalPatchesEnabled()) {
|
||||||
widget.isSelected = false;
|
widget.isSelected = false;
|
||||||
widget.toast.showBottom('patchItem.unsupportedPatchVersion');
|
widget.toast.showBottom('patchItem.unsupportedPatchVersion');
|
||||||
} else if (widget.isChangeEnabled) {
|
} else if (widget.isChangeEnabled) {
|
||||||
widget.isSelected = !widget.isSelected;
|
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);
|
widget.onChanged(widget.isSelected);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Column(
|
child: Row(
|
||||||
children: <Widget>[
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
Row(
|
children: [
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
Transform.scale(
|
||||||
children: <Widget>[
|
scale: 1.2,
|
||||||
Flexible(
|
child: Checkbox(
|
||||||
|
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) {
|
||||||
|
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(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: <Widget>[
|
children: [
|
||||||
Row(
|
Text(
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
|
||||||
children: <Widget>[
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
widget.simpleName,
|
widget.simpleName,
|
||||||
maxLines: 2,
|
maxLines: 2,
|
||||||
overflow: TextOverflow.visible,
|
overflow: TextOverflow.visible,
|
||||||
@ -90,10 +136,6 @@ class _PatchItemState extends State<PatchItem> {
|
|||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
Text(
|
Text(
|
||||||
widget.description,
|
widget.description,
|
||||||
softWrap: true,
|
softWrap: true,
|
||||||
@ -105,89 +147,44 @@ class _PatchItemState extends State<PatchItem> {
|
|||||||
.onSecondaryContainer,
|
.onSecondaryContainer,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
if (widget.description.isNotEmpty)
|
||||||
),
|
Align(
|
||||||
),
|
alignment: Alignment.topLeft,
|
||||||
Transform.scale(
|
child: Wrap(
|
||||||
scale: 1.2,
|
spacing: 8,
|
||||||
child: Checkbox(
|
runSpacing: 4,
|
||||||
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);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
children: [
|
children: [
|
||||||
if (widget.isUnsupported &&
|
if (widget.isUnsupported &&
|
||||||
widget._managerAPI.areExperimentalPatchesEnabled())
|
widget._managerAPI
|
||||||
Padding(
|
.areExperimentalPatchesEnabled())
|
||||||
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(
|
||||||
padding: const EdgeInsets.only(top: 8),
|
padding: const EdgeInsets.only(top: 8),
|
||||||
child: TextButton.icon(
|
child: TextButton.icon(
|
||||||
label: I18nText('new'),
|
label: I18nText('warning'),
|
||||||
icon: const Icon(Icons.star, size: 20.0),
|
icon: const Icon(
|
||||||
onPressed: () => _showNewPatchDialog(),
|
Icons.warning_amber_outlined,
|
||||||
|
size: 20.0,
|
||||||
|
),
|
||||||
|
onPressed: () =>
|
||||||
|
_showUnsupportedWarningDialog(),
|
||||||
style: ButtonStyle(
|
style: ButtonStyle(
|
||||||
shape: MaterialStateProperty.all(
|
shape: MaterialStateProperty.all(
|
||||||
RoundedRectangleBorder(
|
RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius:
|
||||||
|
BorderRadius.circular(8),
|
||||||
side: BorderSide(
|
side: BorderSide(
|
||||||
color: Theme.of(context).colorScheme.secondary,
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.secondary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
backgroundColor: MaterialStateProperty.all(
|
backgroundColor:
|
||||||
|
MaterialStateProperty.all(
|
||||||
Colors.transparent,
|
Colors.transparent,
|
||||||
),
|
),
|
||||||
foregroundColor: MaterialStateProperty.all(
|
foregroundColor:
|
||||||
|
MaterialStateProperty.all(
|
||||||
Theme.of(context).colorScheme.secondary,
|
Theme.of(context).colorScheme.secondary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -195,7 +192,16 @@ class _PatchItemState extends State<PatchItem> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
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: {
|
translationParams: {
|
||||||
'packageVersion': widget.packageVersion,
|
'packageVersion': widget.packageVersion,
|
||||||
'supportedVersions':
|
'supportedVersions':
|
||||||
'\u2022 ${widget.supportedPackageVersions.reversed.join('\n\u2022 ')}',
|
'• ${widget.supportedPackageVersions.reversed.join('\n• ')}',
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
@ -227,14 +233,14 @@ class _PatchItemState extends State<PatchItem> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _showNewPatchDialog() {
|
Future<void> _showUnsupportedRequiredOptionDialog() {
|
||||||
return showDialog(
|
return showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
title: I18nText('patchItem.newPatch'),
|
title: I18nText('notice'),
|
||||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||||
content: I18nText(
|
content: I18nText(
|
||||||
'patchItem.newPatchDialogText',
|
'patchItem.unsupportedRequiredOption',
|
||||||
),
|
),
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
CustomMaterialButton(
|
CustomMaterialButton(
|
||||||
|
@ -1,73 +1,387 @@
|
|||||||
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_i18n/flutter_i18n.dart';
|
import 'package:flutter_i18n/flutter_i18n.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:revanced_manager/models/patch.dart';
|
||||||
|
import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
|
||||||
|
|
||||||
class OptionsTextField extends StatelessWidget {
|
class BooleanPatchOption extends StatelessWidget {
|
||||||
const OptionsTextField({Key? key, required this.hint}) : super(key: key);
|
const BooleanPatchOption({
|
||||||
final String hint;
|
super.key,
|
||||||
|
required this.patchOption,
|
||||||
|
required this.removeOption,
|
||||||
|
required this.onChanged,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Option patchOption;
|
||||||
|
final void Function(Option option) removeOption;
|
||||||
|
final void Function(dynamic value, Option option) onChanged;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final size = MediaQuery.sizeOf(context);
|
final ValueNotifier patchOptionValue = ValueNotifier(patchOption.value);
|
||||||
final sHeight = size.height;
|
return PatchOption(
|
||||||
final sWidth = size.width;
|
widget: Align(
|
||||||
return Container(
|
alignment: Alignment.bottomLeft,
|
||||||
margin: const EdgeInsets.only(top: 12, bottom: 6),
|
child: ValueListenableBuilder(
|
||||||
padding: EdgeInsets.zero,
|
valueListenable: patchOptionValue,
|
||||||
child: TextField(
|
builder: (context, value, child) {
|
||||||
decoration: InputDecoration(
|
return Switch(
|
||||||
constraints: BoxConstraints(
|
value: value ?? false,
|
||||||
maxHeight: sHeight * 0.05,
|
onChanged: (bool value) {
|
||||||
maxWidth: sWidth * 1,
|
patchOptionValue.value = value;
|
||||||
),
|
onChanged(value, patchOption);
|
||||||
border: const OutlineInputBorder(),
|
},
|
||||||
labelText: hint,
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
patchOption: patchOption,
|
||||||
|
removeOption: (Option option) {
|
||||||
|
removeOption(option);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class OptionsFilePicker extends StatelessWidget {
|
class IntAndStringPatchOption extends StatelessWidget {
|
||||||
const OptionsFilePicker({Key? key, required this.optionName})
|
const IntAndStringPatchOption({
|
||||||
: super(key: key);
|
super.key,
|
||||||
final String optionName;
|
required this.patchOption,
|
||||||
|
required this.removeOption,
|
||||||
|
required this.onChanged,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Option patchOption;
|
||||||
|
final void Function(Option option) removeOption;
|
||||||
|
final void Function(dynamic value, Option option) onChanged;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Padding(
|
final ValueNotifier patchOptionValue = ValueNotifier(patchOption.value);
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
return PatchOption(
|
||||||
child: Row(
|
widget: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: <Widget>[
|
children: [
|
||||||
|
TextFieldForPatchOption(
|
||||||
|
value: patchOption.value,
|
||||||
|
optionType: patchOption.optionClassType,
|
||||||
|
onChanged: (value) {
|
||||||
|
patchOptionValue.value = value;
|
||||||
|
onChanged(value, patchOption);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ValueListenableBuilder(
|
||||||
|
valueListenable: patchOptionValue,
|
||||||
|
builder: (context, value, child) {
|
||||||
|
if (patchOption.required && value == null) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children:[
|
||||||
|
const SizedBox(height: 8),
|
||||||
I18nText(
|
I18nText(
|
||||||
optionName,
|
'patchOptionsView.requiredOption',
|
||||||
child: Text(
|
child: Text(
|
||||||
'',
|
'',
|
||||||
style: GoogleFonts.inter(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
ElevatedButton(
|
|
||||||
style: ButtonStyle(
|
|
||||||
backgroundColor: MaterialStateProperty.all(
|
|
||||||
Theme.of(context).colorScheme.primary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onPressed: () {
|
|
||||||
// pick files
|
|
||||||
},
|
|
||||||
child: Text(
|
|
||||||
'Select File',
|
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Theme.of(context).textTheme.bodyLarge?.color,
|
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 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) {
|
||||||
|
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',
|
||||||
|
),
|
||||||
|
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);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -94,21 +94,49 @@ class SExportSection extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
subtitle: I18nText('settingsView.resetStoredPatchesHint'),
|
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(
|
return showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
title: I18nText('settingsView.resetStoredPatchesDialogTitle'),
|
title: I18nText(dialogTitle),
|
||||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||||
content: I18nText(
|
content: I18nText(dialogText),
|
||||||
'settingsView.resetStoredPatchesDialogText',
|
|
||||||
),
|
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
CustomMaterialButton(
|
CustomMaterialButton(
|
||||||
isFilled: false,
|
isFilled: false,
|
||||||
@ -119,7 +147,7 @@ class SExportSection extends StatelessWidget {
|
|||||||
label: I18nText('yesButton'),
|
label: I18nText('yesButton'),
|
||||||
onPressed: () => {
|
onPressed: () => {
|
||||||
Navigator.of(context).pop(),
|
Navigator.of(context).pop(),
|
||||||
_settingsViewModel.resetSelectedPatches(),
|
dialogAction(),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import 'package:revanced_manager/app/app.locator.dart';
|
import 'package:revanced_manager/app/app.locator.dart';
|
||||||
import 'package:revanced_manager/models/patch.dart';
|
import 'package:revanced_manager/models/patch.dart';
|
||||||
import 'package:revanced_manager/models/patched_application.dart';
|
import 'package:revanced_manager/models/patched_application.dart';
|
||||||
|
import 'package:revanced_manager/services/manager_api.dart';
|
||||||
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
|
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
|
||||||
|
|
||||||
bool isPatchSupported(Patch patch) {
|
bool isPatchSupported(Patch patch) {
|
||||||
@ -12,3 +13,49 @@ bool isPatchSupported(Patch patch) {
|
|||||||
(pack.versions.isEmpty || pack.versions.contains(app.version)),
|
(pack.versions.isEmpty || pack.versions.contains(app.version)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool hasUnsupportedRequiredOption(List<Option> options, Patch patch) {
|
||||||
|
final List<String> requiredOptionsType = [];
|
||||||
|
final List<String> supportedOptionsType = [
|
||||||
|
'StringPatchOption',
|
||||||
|
'BooleanPatchOption',
|
||||||
|
'IntPatchOption',
|
||||||
|
'StringListPatchOption',
|
||||||
|
'IntListPatchOption',
|
||||||
|
'LongListPatchOption',
|
||||||
|
];
|
||||||
|
for (final Option option in options) {
|
||||||
|
if (option.required &&
|
||||||
|
option.value == null &&
|
||||||
|
locator<ManagerAPI>()
|
||||||
|
.getPatchOption(
|
||||||
|
locator<PatcherViewModel>().selectedApp!.packageName,
|
||||||
|
patch.name,
|
||||||
|
option.key,
|
||||||
|
) == null) {
|
||||||
|
requiredOptionsType.add(option.optionClassType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (final String optionType in requiredOptionsType) {
|
||||||
|
if (!supportedOptionsType.contains(optionType)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Option> getNullRequiredOptions(List<Patch> patches, String packageName) {
|
||||||
|
final List<Option> requiredNullOptions = [];
|
||||||
|
for (final patch in patches) {
|
||||||
|
for (final patchOption in patch.options) {
|
||||||
|
if (!patch.excluded &&
|
||||||
|
patchOption.required &&
|
||||||
|
patchOption.value == null &&
|
||||||
|
locator<ManagerAPI>()
|
||||||
|
.getPatchOption(packageName, patch.name, patchOption.key) == null) {
|
||||||
|
requiredNullOptions.add(patchOption);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return requiredNullOptions;
|
||||||
|
}
|
||||||
|
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')
|
Loading…
Reference in New Issue
Block a user