Merge branch 'dev' into feat/patch-history

This commit is contained in:
Benjamin Halko 2023-11-01 16:29:18 -07:00
commit d0103a33d9
No known key found for this signature in database
GPG Key ID: 790C70040EB331A0
33 changed files with 665 additions and 561 deletions

View File

@ -23,7 +23,7 @@ jobs:
- name: Setup JDK
uses: actions/setup-java@v3
with:
java-version: '11'
java-version: '17'
distribution: 'zulu'
- name: Setup Flutter
uses: subosito/flutter-action@v2

View File

@ -12,10 +12,10 @@ jobs:
- uses: actions/checkout@v4
- name: Set env
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
- name: Set up JDK 11
- name: Set up JDK
uses: actions/setup-java@v3
with:
java-version: "11"
java-version: "17"
distribution: "zulu"
- uses: subosito/flutter-action@v2
with:

View File

@ -26,30 +26,26 @@ apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android {
compileSdkVersion flutter.compileSdkVersion
compileSdk 34
ndkVersion flutter.ndkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = '11'
jvmTarget = '17'
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
defaultConfig {
applicationId "app.revanced.manager.flutter"
minSdkVersion 26
targetSdkVersion 33
minSdk 26
targetSdk 34
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
buildTypes {
release {
shrinkResources false
@ -71,10 +67,21 @@ android {
}
}
}
packagingOptions {
exclude '/prebuilt/**'
jniLibs {
useLegacyPackaging true
excludes += ['/prebuilt/**']
}
resources {
excludes += ['/prebuilt/**']
}
}
namespace 'app.revanced.manager.flutter'
}
kotlin {
jvmToolchain(17)
}
flutter {
@ -85,7 +92,7 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
// ReVanced
implementation "app.revanced:revanced-patcher:17.0.0"
implementation "app.revanced:revanced-patcher:19.0.0"
// Signing & aligning
implementation("org.bouncycastle:bcpkix-jdk15on:1.70")

View File

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

View File

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

View File

@ -15,7 +15,9 @@ import app.revanced.patcher.patch.PatchResult
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import kotlinx.coroutines.InternalCoroutinesApi
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.runBlocking
import org.json.JSONArray
import org.json.JSONObject
@ -25,6 +27,7 @@ import java.io.StringWriter
import java.util.logging.LogRecord
import java.util.logging.Logger
class MainActivity : FlutterActivity() {
private val handler = Handler(Looper.getMainLooper())
private lateinit var installerChannel: MethodChannel
@ -98,8 +101,10 @@ class MainActivity : FlutterActivity() {
val cacheDirPath = call.argument<String>("cacheDirPath")!!
try {
val patchBundleFile = File(patchBundleFilePath)
patchBundleFile.setWritable(false)
patches = PatchBundleLoader.Dex(
File(patchBundleFilePath),
patchBundleFile,
optimizedDexDirectory = File(cacheDirPath)
)
} catch (ex: Exception) {
@ -131,24 +136,34 @@ class MainActivity : FlutterActivity() {
})
put("options", JSONArray().apply {
it.options.values.forEach { option ->
val optionJson = JSONObject().apply option@{
JSONObject().apply {
put("key", option.key)
put("title", option.title)
put("description", option.description)
put("required", option.required)
when (val value = option.value) {
null -> put("value", null)
is Array<*> -> put("value", JSONArray().apply {
fun JSONObject.putValue(
value: Any?,
key: String = "value"
) = if (value is Array<*>) put(
key,
JSONArray().apply {
value.forEach { put(it) }
})
else -> put("value", option.value)
}
else put(key, value)
put("optionClassType", option::class.simpleName)
}
put(optionJson)
putValue(option.default)
option.values?.let { values ->
put("values",
JSONObject().apply {
values.forEach { (key, value) ->
putValue(value, key)
}
})
} ?: put("values", null)
put("valueType", option.valueType)
}.let(::put)
}
})
}.let(::put)
@ -161,6 +176,7 @@ class MainActivity : FlutterActivity() {
}
}
@OptIn(InternalCoroutinesApi::class)
private fun runPatcher(
result: MethodChannel.Result,
originalFilePath: String,
@ -283,12 +299,12 @@ class MainActivity : FlutterActivity() {
acceptPatches(patches)
runBlocking {
apply(false).collect { patchResult: PatchResult ->
apply(false).collect(FlowCollector { patchResult: PatchResult ->
if (cancel) {
handler.post { stopResult!!.success(null) }
this.cancel()
this@apply.close()
return@collect
return@FlowCollector
}
val msg = patchResult.exception?.let {
@ -301,7 +317,7 @@ class MainActivity : FlutterActivity() {
updateProgress(progress, "", msg)
progress += progressStep
}
})
}
}

View File

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

View File

@ -1,12 +1,12 @@
buildscript {
ext.kotlin_version = '1.9.0'
ext.kotlin_version = '1.9.10'
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.1.3'
classpath 'com.android.tools.build:gradle:8.1.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
@ -22,12 +22,13 @@ allprojects {
}
}
rootProject.buildDir = '../build'
layout.buildDirectory.set(file("../build"))
var root = layout.buildDirectory.get().asFile.absolutePath
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
project.layout.buildDirectory.set(file("$root/${project.name}"))
project.evaluationDependsOn(':app')
}
tasks.register("clean", Delete) {
delete rootProject.buildDir
delete layout.buildDirectory
}

View File

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

View File

@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-all.zip

View File

@ -137,6 +137,7 @@
"setRequiredOption": "Some patches require options to be set:\n\n{patches}\n\nPlease set them before continuing."
},
"patchOptionsView": {
"customValue": "Custom value",
"resetOptionsTooltip": "Reset patch options",
"viewTitle": "Patch options",
"saveOptions": "Save",

View File

@ -9,23 +9,13 @@ This page will guide you through building ReVanced Manager from source.
```sh
git clone https://github.com/revanced/revanced-manager.git && cd revanced-manager
```
3. Create a GitHub personal access token with the `read:packages` scope [here](https://github.com/settings/tokens/new?scopes=read:packages&description=ReVanced)
4. Add your GitHub username and the token to `~/android/gradle.properties`
```properties
gpr.user = YourUsername
gpr.key = ghp_longrandomkey
```
5. Get dependencies
3. Get dependencies
```sh
flutter pub get
```
6. Delete conflicting outputs
4. Delete conflicting outputs
```sh
flutter packages pub run build_runner build --delete-conflicting-outputs
@ -34,7 +24,7 @@ This page will guide you through building ReVanced Manager from source.
> [!Note]
> Must be run every time you sync your local repository with the remote repository.
7. Build the APK
5. Build the APK
```sh
flutter build apk

Binary file not shown.

View File

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

35
gradlew vendored
View File

@ -55,7 +55,7 @@
# 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
# https://github.com/gradle/gradle/blob/HEAD/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/.
@ -80,13 +80,11 @@ do
esac
done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
# This is normally unused
# shellcheck disable=SC2034
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"'
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@ -133,22 +131,29 @@ 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.
if ! command -v java >/dev/null 2>&1
then
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
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
@ -193,11 +198,15 @@ if "$cygwin" || "$msys" ; then
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.
# 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"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \

1
gradlew.bat vendored
View File

@ -26,6 +26,7 @@ if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%

View File

@ -11,12 +11,10 @@ import 'package:revanced_manager/services/revanced_api.dart';
import 'package:revanced_manager/ui/theme/dynamic_theme_builder.dart';
import 'package:revanced_manager/ui/views/navigation/navigation_view.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:stacked_themes/stacked_themes.dart';
import 'package:timezone/data/latest.dart' as tz;
late SharedPreferences prefs;
Future main() async {
await ThemeManager.initialise();
await setupLocator();
WidgetsFlutterBinding.ensureInitialized();
await locator<ManagerAPI>().initialize();

View File

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

View File

@ -1,6 +1,7 @@
import 'dart:convert';
import 'dart:io';
import 'package:device_apps/device_apps.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_i18n/widgets/I18nText.dart';
@ -33,6 +34,7 @@ class ManagerAPI {
Patch? selectedPatch;
BuildContext? ctx;
bool isRooted = false;
bool isDynamicThemeAvailable = false;
String storedPatchesFile = '/selected-patches.json';
String keystoreFile =
'/sdcard/Android/data/app.revanced.manager.flutter/files/revanced-manager.keystore';
@ -59,10 +61,16 @@ class ManagerAPI {
Future<void> initialize() async {
_prefs = await SharedPreferences.getInstance();
isRooted = await _rootAPI.isRooted();
isDynamicThemeAvailable = (await getSdkVersion()) >= 31; // ANDROID_12_SDK_VERSION = 31
storedPatchesFile =
(await getApplicationDocumentsDirectory()).path + storedPatchesFile;
}
Future<int> getSdkVersion() async {
final AndroidDeviceInfo info = await DeviceInfoPlugin().androidInfo;
return info.version.sdkInt;
}
String getApiUrl() {
return _prefs.getString('apiUrl') ?? defaultApiUrl;
}
@ -71,7 +79,6 @@ class ManagerAPI {
if (url.isEmpty || url == ' ') {
url = defaultApiUrl;
}
await _revancedAPI.initialize(url);
await _revancedAPI.clearAllCache();
await _prefs.setString('apiUrl', url);
}

View File

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

View File

@ -1,9 +1,9 @@
import 'dart:io';
import 'package:device_apps/device_apps.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_file_dialog/flutter_file_dialog.dart';
import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/models/patch.dart';
@ -181,13 +181,14 @@ class AppSelectorViewModel extends BaseViewModel {
Future<void> selectAppFromStorage(BuildContext context) async {
try {
final FilePickerResult? result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['apk'],
final String? result = await FlutterFileDialog.pickFile(
params: const OpenFileDialogParams(
fileExtensionsFilter: ['apk'],
),
);
if (result != null && result.files.single.path != null) {
final File apkFile = File(result.files.single.path!);
final List<String> pathSplit = result.files.single.path!.split('/');
if (result != null) {
final File apkFile = File(result);
final List<String> pathSplit = result.split('/');
pathSplit.removeLast();
final Directory filePickerCacheDir = Directory(pathSplit.join('/'));
final Iterable<File> deletableFiles =
@ -207,7 +208,7 @@ class AppSelectorViewModel extends BaseViewModel {
name: application.appName,
packageName: application.packageName,
version: application.versionName!,
apkFilePath: result.files.single.path!,
apkFilePath: result,
icon: application.icon,
patchDate: DateTime.now(),
isFromStorage: true,

View File

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

View File

@ -19,7 +19,7 @@ import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
import 'package:revanced_manager/utils/about_info.dart';
import 'package:screenshot_callback/screenshot_callback.dart';
import 'package:stacked/stacked.dart';
import 'package:wakelock/wakelock.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
class InstallerViewModel extends BaseViewModel {
final ManagerAPI _managerAPI = locator<ManagerAPI>();
@ -75,7 +75,7 @@ class InstallerViewModel extends BaseViewModel {
screenshotDetected(context);
}
});
await Wakelock.enable();
await WakelockPlus.enable();
await handlePlatformChannelMethods();
await runPatcher(context);
}
@ -175,7 +175,7 @@ class InstallerViewModel extends BaseViewModel {
} // ignore
}
}
await Wakelock.disable();
await WakelockPlus.disable();
} on Exception catch (e) {
if (kDebugMode) {
print(e);
@ -187,7 +187,6 @@ class InstallerViewModel extends BaseViewModel {
final info = await AboutInfo.getInfo();
final formattedLogs = [
'```',
'- Device Info',
'ReVanced Manager: ${info['version']}',
'Build: ${info['flavor']}',
@ -210,7 +209,6 @@ class InstallerViewModel extends BaseViewModel {
'\n- Logs',
logs,
'```',
];
Clipboard.setData(ClipboardData(text: formattedLogs.join('\n')));

View File

@ -61,8 +61,8 @@ class PatchOptionsView extends StatelessWidget {
child: Column(
children: [
for (final Option option in model.visibleOptions)
if (option.optionClassType == 'StringPatchOption' ||
option.optionClassType == 'IntPatchOption')
if (option.valueType == 'String' ||
option.valueType == 'Int')
IntAndStringPatchOption(
patchOption: option,
removeOption: (option) {
@ -72,7 +72,7 @@ class PatchOptionsView extends StatelessWidget {
model.modifyOptions(value, option);
},
)
else if (option.optionClassType == 'BooleanPatchOption')
else if (option.valueType == 'Boolean')
BooleanPatchOption(
patchOption: option,
removeOption: (option) {
@ -82,10 +82,10 @@ class PatchOptionsView extends StatelessWidget {
model.modifyOptions(value, option);
},
)
else if (option.optionClassType ==
'StringListPatchOption' ||
option.optionClassType == 'IntListPatchOption' ||
option.optionClassType == 'LongListPatchOption')
else if (option.valueType ==
'StringArray' ||
option.valueType == 'IntArray' ||
option.valueType == 'LongArray')
IntStringLongListPatchOption(
patchOption: option,
removeOption: (option) {

View File

@ -62,7 +62,10 @@ class PatchOptionsViewModel extends BaseViewModel {
for (final Option option in options) {
if (!visibleOptions.any((vOption) => vOption.key == option.key)) {
_managerAPI.clearPatchOption(
selectedApp, _managerAPI.selectedPatch!.name, option.key);
selectedApp,
_managerAPI.selectedPatch!.name,
option.key,
);
}
}
for (final Option option in visibleOptions) {
@ -70,7 +73,10 @@ class PatchOptionsViewModel extends BaseViewModel {
requiredNullOptions.add(option);
} else {
_managerAPI.setPatchOption(
option, _managerAPI.selectedPatch!.name, selectedApp);
option,
_managerAPI.selectedPatch!.name,
selectedApp,
);
}
}
if (requiredNullOptions.isNotEmpty) {
@ -89,7 +95,8 @@ class PatchOptionsViewModel extends BaseViewModel {
final Option modifiedOption = Option(
title: option.title,
description: option.description,
optionClassType: option.optionClassType,
values: option.values,
valueType: option.valueType,
value: value,
required: option.required,
key: option.key,
@ -107,7 +114,8 @@ class PatchOptionsViewModel extends BaseViewModel {
final Option defaultOption = Option(
title: option.title,
description: option.description,
optionClassType: option.optionClassType,
values: option.values,
valueType: option.valueType,
value: option.value is List ? option.value.toList() : option.value,
required: option.required,
key: option.key,
@ -172,21 +180,27 @@ class PatchOptionsViewModel extends BaseViewModel {
},
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
child: Row(
children: [
Text(
e.title,
style: const TextStyle(
fontSize: 16,
),
),
const SizedBox(height: 4),
Text(
e.description,
style: TextStyle(
fontSize: 14,
color: Theme.of(context).colorScheme.onSurface,
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
e.title,
style: const TextStyle(
fontSize: 16,
),
),
const SizedBox(height: 4),
Text(
e.description,
style: TextStyle(
fontSize: 14,
color: Theme.of(context).colorScheme.onSurface,
),
),
],
),
),
],
@ -229,7 +243,10 @@ Future<void> showRequiredOptionNullDialog(
locator<PatcherViewModel>().notifyListeners();
for (final option in options) {
managerAPI.clearPatchOption(
selectedApp, managerAPI.selectedPatch!.name, option.key);
selectedApp,
managerAPI.selectedPatch!.name,
option.key,
);
}
Navigator.of(context)
..pop()

View File

@ -3,9 +3,7 @@ import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/services/manager_api.dart';
import 'package:revanced_manager/ui/views/patches_selector/patches_selector_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/patchesSelectorView/patch_item.dart';
import 'package:revanced_manager/ui/widgets/shared/search_bar.dart';
import 'package:revanced_manager/utils/check_for_supported_patch.dart';
import 'package:stacked/stacked.dart';
class PatchesSelectorView extends StatefulWidget {
@ -182,187 +180,39 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
),
],
),
if (model.newPatchExists())
if (model.getQueriedPatches(_query).any((patch) => model.isPatchNew(patch)))
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(
vertical: 10.0,
),
child: Container(
padding: const EdgeInsets.only(
top: 10.0,
bottom: 10.0,
left: 5.0,
),
child: I18nText(
'patchesSelectorView.newPatches',
child: Text(
'',
style: TextStyle(
color: Theme.of(context)
.colorScheme
.primary,
),
),
),
),
),
model.getPatchCategory(context, 'patchesSelectorView.newPatches'),
...model.getQueriedPatches(_query).map((patch) {
if (model.isPatchNew(patch)) {
return PatchItem(
name: patch.name,
simpleName: patch.getSimpleName(),
description: patch.description ?? '',
packageVersion:
model.getAppInfo().version,
supportedPackageVersions:
model.getSupportedVersions(patch),
isUnsupported: !isPatchSupported(patch),
isChangeEnabled:
_managerAPI.isPatchesChangeEnabled(),
hasUnsupportedPatchOption:
hasUnsupportedRequiredOption(
patch.options,
patch,
),
options: patch.options,
isSelected: model.isSelected(patch),
navigateToOptions: (options) =>
model.navigateToPatchOptions(
options,
patch,
),
onChanged: (value) => model.selectPatch(
patch,
value,
context,
),
);
return model.getPatchItem(context, patch);
} else {
return Container();
}
}),
Padding(
padding: const EdgeInsets.symmetric(
vertical: 10.0,
),
child: Container(
padding: const EdgeInsets.only(
top: 10.0,
bottom: 10.0,
left: 5.0,
),
child: I18nText(
'patchesSelectorView.patches',
child: Text(
'',
style: TextStyle(
color: Theme.of(context)
.colorScheme
.primary,
),
),
),
),
),
if (model.getQueriedPatches(_query).any((patch) => !model.isPatchNew(patch) && patch.compatiblePackages.isNotEmpty))
model.getPatchCategory(context, 'patchesSelectorView.patches'),
],
),
...model.getQueriedPatches(_query).map(
(patch) {
if (patch.compatiblePackages.isNotEmpty) {
return PatchItem(
name: patch.name,
simpleName: patch.getSimpleName(),
description: patch.description ?? '',
packageVersion: model.getAppInfo().version,
supportedPackageVersions:
model.getSupportedVersions(patch),
isUnsupported: !isPatchSupported(patch),
isChangeEnabled:
_managerAPI.isPatchesChangeEnabled(),
hasUnsupportedPatchOption:
hasUnsupportedRequiredOption(
patch.options,
patch,
),
options: patch.options,
isSelected: model.isSelected(patch),
navigateToOptions: (options) =>
model.navigateToPatchOptions(
options,
patch,
),
onChanged: (value) => model.selectPatch(
patch,
value,
context,
),
);
if (patch.compatiblePackages.isNotEmpty && !model.isPatchNew(patch)) {
return model.getPatchItem(context, patch);
} else {
return Container();
}
},
),
if (_managerAPI.areUniversalPatchesEnabled())
if (model.getQueriedPatches(_query).any((patch) => patch.compatiblePackages.isEmpty))
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(
vertical: 10.0,
),
child: Container(
padding: const EdgeInsets.only(
top: 10.0,
bottom: 10.0,
left: 5.0,
),
child: I18nText(
'patchesSelectorView.universalPatches',
child: Text(
'',
style: TextStyle(
color: Theme.of(context)
.colorScheme
.primary,
),
),
),
),
),
model.getPatchCategory(context, 'patchesSelectorView.universalPatches'),
...model.getQueriedPatches(_query).map((patch) {
if (patch.compatiblePackages.isEmpty) {
return PatchItem(
name: patch.name,
simpleName: patch.getSimpleName(),
description: patch.description ?? '',
packageVersion:
model.getAppInfo().version,
supportedPackageVersions:
model.getSupportedVersions(patch),
isUnsupported: !isPatchSupported(patch),
isChangeEnabled:
_managerAPI.isPatchesChangeEnabled(),
hasUnsupportedPatchOption:
hasUnsupportedRequiredOption(
patch.options,
patch,
),
options: patch.options,
isSelected: model.isSelected(patch),
navigateToOptions: (options) =>
model.navigateToPatchOptions(
options,
patch,
),
onChanged: (value) => model.selectPatch(
patch,
value,
context,
),
);
if (patch.compatiblePackages.isEmpty && !model.isPatchNew(patch)) {
return model.getPatchItem(context, patch);
} else {
return Container();
}

View File

@ -9,6 +9,7 @@ import 'package:revanced_manager/services/manager_api.dart';
import 'package:revanced_manager/services/patcher_api.dart';
import 'package:revanced_manager/services/toast.dart';
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/patchesSelectorView/patch_item.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
import 'package:revanced_manager/utils/check_for_supported_patch.dart';
import 'package:stacked/stacked.dart';
@ -224,6 +225,57 @@ class PatchesSelectorViewModel extends BaseViewModel {
}
}
Widget getPatchItem(BuildContext context, Patch patch) {
return PatchItem(
name: patch.name,
simpleName: patch.getSimpleName(),
description: patch.description ?? '',
packageVersion: getAppInfo().version,
supportedPackageVersions: getSupportedVersions(patch),
isUnsupported: !isPatchSupported(patch),
isChangeEnabled: _managerAPI.isPatchesChangeEnabled(),
hasUnsupportedPatchOption: hasUnsupportedRequiredOption(
patch.options,
patch,
),
options: patch.options,
isSelected: isSelected(patch),
navigateToOptions: (options) => navigateToPatchOptions(
options,
patch,
),
onChanged: (value) => selectPatch(
patch,
value,
context,
),
);
}
Widget getPatchCategory(BuildContext context, String category) {
return Padding(
padding: const EdgeInsets.symmetric(
vertical: 10.0,
),
child: Container(
padding: const EdgeInsets.only(
top: 10.0,
bottom: 10.0,
left: 5.0,
),
child: I18nText(
category,
child: Text(
'',
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
),
),
),
),
);
}
PatchedApplication getAppInfo() {
return locator<PatcherViewModel>().selectedApp!;
}
@ -239,12 +291,6 @@ class PatchesSelectorViewModel extends BaseViewModel {
}
}
bool newPatchExists() {
return patches.any(
(patch) => isPatchNew(patch),
);
}
List<String> getSupportedVersions(Patch patch) {
final PatchedApplication app = locator<PatcherViewModel>().selectedApp!;
final Package? package = patch.compatiblePackages.firstWhereOrNull(

View File

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

View File

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

View File

@ -1,9 +1,7 @@
import 'dart:io';
import 'package:cr_file_saver/file_saver.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_file_dialog/flutter_file_dialog.dart';
import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:logcat/logcat.dart';
import 'package:path_provider/path_provider.dart';
@ -14,9 +12,8 @@ import 'package:revanced_manager/services/toast.dart';
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
import 'package:revanced_manager/ui/views/patches_selector/patches_selector_viewmodel.dart';
import 'package:revanced_manager/ui/views/settings/settingsFragment/settings_update_language.dart';
import 'package:revanced_manager/ui/views/settings/settingsFragment/settings_update_theme.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
import 'package:share_extend/share_extend.dart';
import 'package:share_plus/share_plus.dart';
import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart';
@ -29,7 +26,6 @@ class SettingsViewModel extends BaseViewModel {
final Toast _toast = locator<Toast>();
final SUpdateLanguage sUpdateLanguage = SUpdateLanguage();
final SUpdateTheme sUpdateTheme = SUpdateTheme();
void navigateToContributors() {
_navigationService.navigateTo(Routes.contributorsView);
@ -162,10 +158,10 @@ class SettingsViewModel extends BaseViewModel {
if (outFile.existsSync()) {
final String dateTime =
DateTime.now().toString().replaceAll(' ', '_').split('.').first;
await CRFileSaver.saveFileWithDialog(
SaveFileDialogParams(
await FlutterFileDialog.saveFile(
params: SaveFileDialogParams(
sourceFilePath: outFile.path,
destinationFileName: 'selected_patches_$dateTime.json',
fileName: 'selected_patches_$dateTime.json',
),
);
_toast.showBottom('settingsView.exportedPatches');
@ -182,12 +178,13 @@ class SettingsViewModel extends BaseViewModel {
Future<void> importPatches(BuildContext context) async {
if (isPatchesChangeEnabled()) {
try {
final FilePickerResult? result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['json'],
final String? result = await FlutterFileDialog.pickFile(
params: const OpenFileDialogParams(
fileExtensionsFilter: ['json'],
),
);
if (result != null && result.files.single.path != null) {
final File inFile = File(result.files.single.path!);
if (result != null) {
final File inFile = File(result);
inFile.copySync(_managerAPI.storedPatchesFile);
inFile.delete();
if (_patcherViewModel.selectedApp != null) {
@ -212,10 +209,10 @@ class SettingsViewModel extends BaseViewModel {
if (outFile.existsSync()) {
final String dateTime =
DateTime.now().toString().replaceAll(' ', '_').split('.').first;
await CRFileSaver.saveFileWithDialog(
SaveFileDialogParams(
await FlutterFileDialog.saveFile(
params: SaveFileDialogParams(
sourceFilePath: outFile.path,
destinationFileName: 'keystore_$dateTime.keystore',
fileName: 'keystore_$dateTime.keystore',
),
);
_toast.showBottom('settingsView.exportedKeystore');
@ -231,9 +228,9 @@ class SettingsViewModel extends BaseViewModel {
Future<void> importKeystore() async {
try {
final FilePickerResult? result = await FilePicker.platform.pickFiles();
if (result != null && result.files.single.path != null) {
final File inFile = File(result.files.single.path!);
final String? result = await FlutterFileDialog.pickFile();
if (result != null) {
final File inFile = File(result);
inFile.copySync(_managerAPI.keystoreFile);
_toast.showBottom('settingsView.importedKeystore');
@ -256,11 +253,6 @@ class SettingsViewModel extends BaseViewModel {
_toast.showBottom('settingsView.resetStoredPatches');
}
Future<int> getSdkVersion() async {
final AndroidDeviceInfo info = await DeviceInfoPlugin().androidInfo;
return info.version.sdkInt;
}
Future<void> deleteLogs() async {
final Directory appCacheDir = await getTemporaryDirectory();
final Directory logsDir = Directory('${appCacheDir.path}/logs');
@ -284,6 +276,6 @@ class SettingsViewModel extends BaseViewModel {
File('${logDir.path}/revanced-manager_logcat_$dateTime.log');
final String logs = await Logcat.execute();
logcat.writeAsStringSync(logs);
ShareExtend.share(logcat.path, 'file');
await Share.shareXFiles([XFile(logcat.path)]);
}
}

View File

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

View File

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

View File

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