feat: Add patch options (#1354)

This commit is contained in:
aAbed 2023-10-12 00:00:39 +00:00 committed by GitHub
parent 2abadc73e4
commit ac636670c3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 1889 additions and 397 deletions

View File

@ -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")

View File

@ -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) {

View File

@ -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()
} }

View File

@ -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

View File

@ -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

Binary file not shown.

View 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
View 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
View 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

View File

@ -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),

View File

@ -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);
}

View File

@ -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()) {

View File

@ -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 '';
} }}
}

View File

@ -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),

View File

@ -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();
}
} }
} }
} }

View File

@ -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 = '';

View File

@ -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();

View 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,
),
],
),
),
),
],
),
),
),
);
}
}

View 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'),
},
),
),
);
}

View File

@ -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(

View File

@ -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();

View File

@ -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,

View File

@ -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(

View File

@ -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');

View File

@ -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) {

View File

@ -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);
} }

View File

@ -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(

View File

@ -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);
},
); );
} }
} }

View File

@ -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(),
}, },
), ),
], ],

View File

@ -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
View File

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