diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml index dbc20e29..8424eff4 100644 --- a/.github/workflows/pr-build.yml +++ b/.github/workflows/pr-build.yml @@ -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 diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index 7d31e5a0..94f2921d 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -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: diff --git a/android/app/build.gradle b/android/app/build.gradle index 42a9a7d0..ecf4135c 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -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") diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml index 2abfc8e9..bbd7ee77 100644 --- a/android/app/src/debug/AndroidManifest.xml +++ b/android/app/src/debug/AndroidManifest.xml @@ -1,4 +1,3 @@ - + diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 3a2c4f8c..7fade03b 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,4 @@ - - + @@ -24,8 +22,7 @@ android:name="${applicationName}" android:icon="@mipmap/ic_launcher" android:largeHeap="true" - android:requestLegacyExternalStorage="true" - android:extractNativeLibs="true"> + android:requestLegacyExternalStorage="true"> ("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 - } + }) } } diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml index 2abfc8e9..bbd7ee77 100644 --- a/android/app/src/profile/AndroidManifest.xml +++ b/android/app/src/profile/AndroidManifest.xml @@ -1,4 +1,3 @@ - + diff --git a/android/build.gradle b/android/build.gradle index 0165e070..0a04adab 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -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 } diff --git a/android/gradle.properties b/android/gradle.properties index 4b11638c..21a7e728 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -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 diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 3c472b99..8bc9958a 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -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 diff --git a/assets/i18n/en_US.json b/assets/i18n/en_US.json index b94bf7e3..86b27131 100644 --- a/assets/i18n/en_US.json +++ b/assets/i18n/en_US.json @@ -135,6 +135,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", diff --git a/docs/4_building.md b/docs/4_building.md index d687cfad..b3904c50 100644 --- a/docs/4_building.md +++ b/docs/4_building.md @@ -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 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 249e5832..7f93135c 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ae04661e..3fa8f862 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -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 diff --git a/gradlew b/gradlew index a69d9cb6..1aa94a42 100644 --- a/gradlew +++ b/gradlew @@ -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" \ diff --git a/gradlew.bat b/gradlew.bat index f127cfd4..93e3f59f 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -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% diff --git a/lib/main.dart b/lib/main.dart index 956f1c30..c81701ba 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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().initialize(); diff --git a/lib/models/patch.dart b/lib/models/patch.dart index e9078773..199cbb87 100644 --- a/lib/models/patch.dart +++ b/lib/models/patch.dart @@ -13,12 +13,15 @@ class Patch { }); factory Patch.fromJson(Map json) { - // See: https://github.com/ReVanced/revanced-manager/issues/1364#issuecomment-1760414618 + _migrateV16ToV17(json); + + return _$PatchFromJson(json); + } + + static void _migrateV16ToV17(Map 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 json) => _$OptionFromJson(json); + factory Option.fromJson(Map json) { + _migrateV17ToV19(json); + + return _$OptionFromJson(json); + } + + static void _migrateV17ToV19(Map 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? values; final bool required; - final String optionClassType; + final String valueType; Map toJson() => _$OptionToJson(this); } diff --git a/lib/services/patcher_api.dart b/lib/services/patcher_api.dart index 80a7e470..4266a880 100644 --- a/lib/services/patcher_api.dart +++ b/lib/services/patcher_api.dart @@ -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(String appName, String version) { try { if (outFile != null) { final String newName = _getFileName(appName, version); - CRFileSaver.saveFileWithDialog( - SaveFileDialogParams( + FlutterFileDialog.saveFile( + params: SaveFileDialogParams( sourceFilePath: outFile!.path, - destinationFileName: newName, + fileName: newName, ), ); } @@ -258,7 +258,7 @@ void sharePatchedFile(String appName, String version) { final String newPath = outFile!.path.substring(0, lastSeparator + 1) + newName; final File shareFile = outFile!.copySync(newPath); - ShareExtend.share(shareFile.path, 'file'); + Share.shareXFiles([XFile(shareFile.path)]); } } on Exception catch (e) { if (kDebugMode) { @@ -286,10 +286,10 @@ Future 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, ), ); } diff --git a/lib/ui/views/app_selector/app_selector_viewmodel.dart b/lib/ui/views/app_selector/app_selector_viewmodel.dart index 9d2f9a7b..53c7a239 100644 --- a/lib/ui/views/app_selector/app_selector_viewmodel.dart +++ b/lib/ui/views/app_selector/app_selector_viewmodel.dart @@ -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 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 pathSplit = result.files.single.path!.split('/'); + if (result != null) { + final File apkFile = File(result); + final List pathSplit = result.split('/'); pathSplit.removeLast(); final Directory filePickerCacheDir = Directory(pathSplit.join('/')); final Iterable 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, diff --git a/lib/ui/views/home/home_viewmodel.dart b/lib/ui/views/home/home_viewmodel.dart index b69b99da..b85e49da 100644 --- a/lib/ui/views/home/home_viewmodel.dart +++ b/lib/ui/views/home/home_viewmodel.dart @@ -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'; @@ -64,8 +64,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'); } diff --git a/lib/ui/views/installer/installer_viewmodel.dart b/lib/ui/views/installer/installer_viewmodel.dart index 7fef5e84..73b23027 100644 --- a/lib/ui/views/installer/installer_viewmodel.dart +++ b/lib/ui/views/installer/installer_viewmodel.dart @@ -18,7 +18,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(); @@ -74,7 +74,7 @@ class InstallerViewModel extends BaseViewModel { screenshotDetected(context); } }); - await Wakelock.enable(); + await WakelockPlus.enable(); await handlePlatformChannelMethods(); await runPatcher(); } @@ -171,7 +171,7 @@ class InstallerViewModel extends BaseViewModel { } // ignore } } - await Wakelock.disable(); + await WakelockPlus.disable(); } on Exception catch (e) { if (kDebugMode) { print(e); diff --git a/lib/ui/views/patch_options/patch_options_view.dart b/lib/ui/views/patch_options/patch_options_view.dart index 7a21eb49..e35b849d 100644 --- a/lib/ui/views/patch_options/patch_options_view.dart +++ b/lib/ui/views/patch_options/patch_options_view.dart @@ -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) { diff --git a/lib/ui/views/patch_options/patch_options_viewmodel.dart b/lib/ui/views/patch_options/patch_options_viewmodel.dart index b8813d49..c1da7992 100644 --- a/lib/ui/views/patch_options/patch_options_viewmodel.dart +++ b/lib/ui/views/patch_options/patch_options_viewmodel.dart @@ -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 showRequiredOptionNullDialog( locator().notifyListeners(); for (final option in options) { managerAPI.clearPatchOption( - selectedApp, managerAPI.selectedPatch!.name, option.key); + selectedApp, + managerAPI.selectedPatch!.name, + option.key, + ); } Navigator.of(context) ..pop() diff --git a/lib/ui/views/patches_selector/patches_selector_view.dart b/lib/ui/views/patches_selector/patches_selector_view.dart index 131c5099..eb20cf82 100644 --- a/lib/ui/views/patches_selector/patches_selector_view.dart +++ b/lib/ui/views/patches_selector/patches_selector_view.dart @@ -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 { ), ], ), - 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(); } diff --git a/lib/ui/views/patches_selector/patches_selector_viewmodel.dart b/lib/ui/views/patches_selector/patches_selector_viewmodel.dart index 29b670ca..173c1f53 100644 --- a/lib/ui/views/patches_selector/patches_selector_viewmodel.dart +++ b/lib/ui/views/patches_selector/patches_selector_viewmodel.dart @@ -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().selectedApp!; } @@ -239,12 +291,6 @@ class PatchesSelectorViewModel extends BaseViewModel { } } - bool newPatchExists() { - return patches.any( - (patch) => isPatchNew(patch), - ); - } - List getSupportedVersions(Patch patch) { final PatchedApplication app = locator().selectedApp!; final Package? package = patch.compatiblePackages.firstWhereOrNull( diff --git a/lib/ui/views/settings/settings_viewmodel.dart b/lib/ui/views/settings/settings_viewmodel.dart index bc3988e6..71730088 100644 --- a/lib/ui/views/settings/settings_viewmodel.dart +++ b/lib/ui/views/settings/settings_viewmodel.dart @@ -1,8 +1,7 @@ import 'dart:io'; -import 'package:cr_file_saver/file_saver.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,7 +13,7 @@ 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/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'; @@ -159,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'); @@ -179,12 +178,13 @@ class SettingsViewModel extends BaseViewModel { Future 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) { @@ -209,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'); @@ -228,9 +228,9 @@ class SettingsViewModel extends BaseViewModel { Future 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'); @@ -276,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)]); } } diff --git a/lib/ui/widgets/patchesSelectorView/patch_options_fields.dart b/lib/ui/widgets/patchesSelectorView/patch_options_fields.dart index e02021cf..ae026cb7 100644 --- a/lib/ui/widgets/patchesSelectorView/patch_options_fields.dart +++ b/lib/ui/widgets/patchesSelectorView/patch_options_fields.dart @@ -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 values = patchOption.value ?? []; + final List 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? 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 { 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( + 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); + }, + ), + ], ); } } diff --git a/lib/utils/check_for_supported_patch.dart b/lib/utils/check_for_supported_patch.dart index dc2d1936..19bd30c1 100644 --- a/lib/utils/check_for_supported_patch.dart +++ b/lib/utils/check_for_supported_patch.dart @@ -17,12 +17,12 @@ bool isPatchSupported(Patch patch) { bool hasUnsupportedRequiredOption(List