Compare commits

...

34 Commits

Author SHA1 Message Date
6a12e8f37a build: bump version to v1.13.1 2023-10-15 20:19:23 +03:00
59adb91f5f fix(settings): inverted version compatibility switch 2023-10-15 20:16:58 +03:00
53677e2f39 fix: typo in reset patch selection dialog (#1387) 2023-10-15 20:14:19 +03:00
1c74f43b22 build: bump version to 1.13.0 2023-10-15 18:22:42 +07:00
b4801970e8 chore: merge dev to main (#1384)
ReVanced Manager 1.13.0!!
2023-10-15 18:18:09 +07:00
7a3a6b512f docs(building): use new highlight format 2023-10-15 18:04:19 +07:00
72ea33b6de fix(patch_selector): correct popup menu style
fix: #1372
2023-10-15 17:46:44 +07:00
d97192e0ee refactor(patch_options): disable card tap (#1368) 2023-10-15 16:56:43 +07:00
196d9fe4d2 refactor: reorganize and rename settings (#1307)
Co-authored-by: Pun Butrach <pun.butrach@gmail.com>
Co-authored-by: Ushie <ushiekane@gmail.com>
2023-10-15 16:56:02 +07:00
e960fcc303 feat: add user agent (#1380)
Co-authored-by: Pun Butrach <pun.butrach@gmail.com>
2023-10-15 16:51:31 +07:00
f334da95ff feat: Remove full external storage access (#1381) 2023-10-15 16:44:11 +07:00
5d5f311e36 fix(settings-view): list items jumping on scroll up (#1375) 2023-10-15 16:17:28 +07:00
d577e97758 fix(app-bar): title not hiding completely (#1376) 2023-10-15 16:16:55 +07:00
2dc92e26d3 fix(patches-selector): ignore punctuation marks when searching for patch (#1377) 2023-10-14 19:43:18 +07:00
f4994a36a3 chore: Merge branch dev to main (#1366) 2023-10-13 00:22:02 +02:00
7a785a8163 build: Bump version to v1.12.1 2023-10-13 00:14:14 +02:00
6ad0d860c7 fix: Load patches from older versions of ReVanced Manager correctly 2023-10-13 00:12:27 +02:00
38a2fa55df chore: Merge branch dev to main (#1350) 2023-10-12 21:58:29 +02:00
a21b170b52 build: Bump version to v1.12.0 2023-10-12 21:55:06 +02:00
44265b2362 chore: Setup Crowdin configuration file 2023-10-12 18:51:16 +02:00
069193342b docs: use correct alert syntax 2023-10-12 14:25:13 +02:00
54e9a56cda docs: Add options setting 2023-10-12 14:22:20 +02:00
39bc9227dc docs: Improve readability 2023-10-12 14:20:44 +02:00
ac636670c3 feat: Add patch options (#1354) 2023-10-12 02:00:39 +02:00
2abadc73e4 fix: selected patches order (#1345) 2023-10-10 18:54:42 +07:00
377368f6bf docs: reflect the latest changes of ReVanced Manager (#1349) 2023-10-10 01:07:58 +07:00
4085c10bfc fix: search with package name (#1344) 2023-10-09 11:22:15 +07:00
657ba11e7e chore: Merge branch dev to main (#1340) 2023-10-07 02:04:26 +02:00
a9ae45fe63 build: Bump version to v1.11.2 2023-10-07 02:01:10 +02:00
61bb39b46f build: Bump dependencies to improve merging integrations speed 2023-10-07 02:01:10 +02:00
2ad106f7d7 fix(export-settings): remove boolean workaround 2023-10-07 02:01:09 +02:00
8fd4fe0e55 feat(patcher): improve logs (#1299)
Co-authored-by: Ushie <ushiekane@gmail.com>
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
2023-10-07 02:01:09 +02:00
b1c9aedac3 build: Bump version to v1.11.1 2023-10-05 17:45:56 +02:00
a80415be02 build: Bump dependencies 2023-10-05 17:45:56 +02:00
54 changed files with 2411 additions and 777 deletions

View File

@ -85,7 +85,7 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
// ReVanced
implementation "app.revanced:revanced-patcher:16.0.0"
implementation "app.revanced:revanced-patcher:17.0.0"
// Signing & aligning
implementation("org.bouncycastle:bcpkix-jdk15on:1.70")

View File

@ -30,10 +30,7 @@ class ExportSettingsActivity : Activity() {
val sharedPreferences = getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE)
val allEntries: Map<String, *> = sharedPreferences.getAll()
for ((key, value) in allEntries.entries) {
json.put(
key.replace("flutter.", ""),
if (value is Boolean) if (value) 1 else 0 else value
)
json.put(key.replace("flutter.", ""), value)
}
// Load keystore

View File

@ -22,7 +22,6 @@ import org.json.JSONObject
import java.io.File
import java.io.PrintWriter
import java.io.StringWriter
import java.lang.Error
import java.util.logging.LogRecord
import java.util.logging.Logger
@ -55,6 +54,7 @@ class MainActivity : FlutterActivity() {
val outFilePath = call.argument<String>("outFilePath")
val integrationsPath = call.argument<String>("integrationsPath")
val selectedPatches = call.argument<List<String>>("selectedPatches")
val options = call.argument<Map<String, Map<String, Any>>>("options")
val cacheDirPath = call.argument<String>("cacheDirPath")
val keyStoreFilePath = call.argument<String>("keyStoreFilePath")
val keystorePassword = call.argument<String>("keystorePassword")
@ -66,6 +66,7 @@ class MainActivity : FlutterActivity() {
outFilePath != null &&
integrationsPath != null &&
selectedPatches != null &&
options != null &&
cacheDirPath != null &&
keyStoreFilePath != null &&
keystorePassword != null
@ -79,6 +80,7 @@ class MainActivity : FlutterActivity() {
outFilePath,
integrationsPath,
selectedPatches,
options,
cacheDirPath,
keyStoreFilePath,
keystorePassword
@ -127,6 +129,28 @@ class MainActivity : FlutterActivity() {
put(compatiblePackageJson)
}
})
put("options", JSONArray().apply {
it.options.values.forEach { option ->
val optionJson = JSONObject().apply option@{
put("key", option.key)
put("title", option.title)
put("description", option.description)
put("required", option.required)
when (val value = option.value) {
null -> put("value", null)
is Array<*> -> put("value", JSONArray().apply {
value.forEach { put(it) }
})
else -> put("value", option.value)
}
put("optionClassType", option::class.simpleName)
}
put(optionJson)
}
})
}.let(::put)
}
}.toString().let(result::success)
@ -145,6 +169,7 @@ class MainActivity : FlutterActivity() {
outFilePath: String,
integrationsPath: String,
selectedPatches: List<String>,
options: Map<String, Map<String, Any>>,
cacheDirPath: String,
keyStoreFilePath: String,
keystorePassword: String
@ -182,7 +207,7 @@ class MainActivity : FlutterActivity() {
object : java.util.logging.Handler() {
override fun publish(record: LogRecord) {
if (record.loggerName?.startsWith("app.revanced") != true) return
if (record.loggerName?.startsWith("app.revanced") != true || cancel) return
updateProgress(-1.0, "", record.message)
}
@ -215,6 +240,7 @@ class MainActivity : FlutterActivity() {
cacheDir,
Aapt.binary(applicationContext).absolutePath,
cacheDir.path,
true // TODO: Add option to disable this
)
)
@ -234,6 +260,10 @@ class MainActivity : FlutterActivity() {
isCompatible || patch.compatiblePackages.isNullOrEmpty()
compatibleOrUniversal && selectedPatches.any { it == patch.name }
}.onEach { patch ->
options[patch.name]?.forEach { (key, value) ->
patch.options[key] = value
}
}
if (cancel) {

View File

@ -16,11 +16,7 @@ allprojects {
google()
mavenCentral()
maven {
url = uri("https://maven.pkg.github.com/revanced/revanced-patcher")
credentials {
username = (project.findProperty("gpr.user") ?: System.getenv("GITHUB_ACTOR")) as String
password = (project.findProperty("gpr.key") ?: System.getenv("GITHUB_TOKEN")) as String
}
url 'https://jitpack.io'
}
mavenLocal()
}

View File

@ -1,7 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=a01b6587e15fe7ed120a0ee299c25982a1eee045abd6a9dd5e216b2f628ef9ac
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.2-bin.zip
networkTimeout=10000
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip

View File

@ -10,9 +10,11 @@
"yesButton": "Yes",
"noButton": "No",
"warning": "Warning",
"options": "Options",
"notice": "Notice",
"noShowAgain": "Don't show this again",
"new": "New",
"add": "Add",
"remove": "Remove",
"navigationView": {
"dashboardTab": "Dashboard",
"patcherTab": "Patcher",
@ -71,7 +73,8 @@
"armv7WarningDialogText": "Patching on ARMv7 devices is not yet supported and might fail. Proceed anyways?",
"removedPatchesWarningDialogText": "The following patches have been removed since the last time you used them.\n\n{patches}\n\nProceed anyways?"
"removedPatchesWarningDialogText": "The following patches have been removed since the last time you used them.\n\n{patches}\n\nProceed anyways?",
"requiredOptionDialogText" : "Some patch options have to be set."
},
"appSelectorCard": {
"widgetTitle": "Select an application",
@ -114,6 +117,8 @@
"viewTitle": "Select patches",
"searchBarHint": "Search patches",
"universalPatches": "Universal patches",
"newPatches": "New patches",
"patches": "Patches",
"doneButton": "Done",
@ -123,20 +128,32 @@
"none": "None",
"noneTooltip": "Deselect all patches",
"loadPatchesSelection": "Load patches selection",
"noSavedPatches": "No saved patches for the selected app.\nPress Done to save current selection.",
"loadPatchesSelection": "Load patch selection",
"noSavedPatches": "No saved patch selection for the selected app.\nPress Done to save the current selection.",
"noPatchesFound": "No patches found for the selected app",
"setRequiredOption": "Some patches require options to be set:\n\n{patches}\n\nPlease set them before continuing."
},
"patchOptionsView": {
"viewTitle": "Patch options",
"saveOptions": "Save",
"selectAllPatchesWarningContent": "You are about to select all patches, that includes non-suggested patches and can cause unwanted behavior."
"addOptions": "Add options",
"deselectPatch": "Deselect patch",
"tooltip": "More input options",
"selectFilePath": "Select file path",
"selectFolder": "Select folder",
"selectOption": "Select option",
"requiredOption": "This option is required",
"unsupportedOption": "This option is not supported",
"requiredOptionNull": "The following options have to be set:\n\n{options}"
},
"patchItem": {
"unsupportedDialogText": "Selecting this patch may result in patching errors.\n\nApp version: {packageVersion}\nSupported versions:\n{supportedVersions}",
"unsupportedPatchVersion": "Patch is not supported for this app version. Enable the experimental toggle in settings to proceed.",
"unsupportedPatchVersion": "Patch is not supported for this app version.",
"unsupportedRequiredOption": "This patch contains a required option that is not supported by this app",
"newPatchDialogText": "This is a new patch that has been added since the last time you have patched this app.",
"newPatch": "New patch",
"patchesChangeWarningDialogText": "It is recommended to use the default selection of patches because changing it may cause unexpected issues.\n\nIf you know what you are doing, you can enable \"Enable changing selection\" in the settings.",
"patchesChangeWarningDialogText": "It is recommended to use the default patch selection and options. Changing them may result in unexpected issues.\n\nYou'll need to turn on \"Allow changing patch selection\" in settings before changing any patch selection.",
"patchesChangeWarningDialogButton": "Use default selection"
},
"installerView": {
@ -158,6 +175,9 @@
"exportApkButtonTooltip": "Export patched APK",
"exportLogButtonTooltip": "Export log",
"screenshotDetected": "A screenshot has been detected. If you are trying to share the log, please share a text copy instead.\n\nCopy log to clipboard?",
"copiedToClipboard": "Copied log to clipboard",
"noExit": "Installer is still running, cannot exit..."
},
"settingsView": {
@ -165,10 +185,9 @@
"appearanceSectionTitle": "Appearance",
"teamSectionTitle": "Team",
"infoSectionTitle": "Info",
"debugSectionTitle": "Debugging",
"advancedSectionTitle": "Advanced",
"exportSectionTitle": "Import & export",
"logsSectionTitle": "Logs",
"themeModeLabel": "App theme",
"systemThemeLabel": "System",
@ -182,15 +201,15 @@
"englishOption": "English",
"sourcesLabel": "Sources",
"sourcesLabelHint": "Configure your custom sources",
"sourcesLabelHint": "Configure your sources",
"sourcesIntegrationsLabel": "Integrations source",
"sourcesResetDialogTitle": "Reset",
"sourcesResetDialogText": "Are you sure you want to reset custom sources to their default values?",
"apiURLResetDialogText": "Are you sure you want to reset API URL to its default value?",
"sourcesUpdateNote": "Note: ReVanced Patches will be updated to the latest version automatically.\n\nThis will reveal your IP address to the server.",
"sourcesResetDialogText": "Are you sure you want to reset your sources to their default values?",
"apiURLResetDialogText": "Are you sure you want to reset your API URL to its default value?",
"sourcesUpdateNote": "Note: Patches will be updated to the latest version automatically.\n\nThis will reveal your IP address to the server.",
"apiURLLabel": "API URL",
"apiURLHint": "Configure your custom API URL",
"apiURLHint": "Configure your API URL",
"selectApiURL": "API URL",
"hostRepositoryLabel": "Repository API",
"orgPatchesLabel": "Patches organization",
@ -200,21 +219,20 @@
"contributorsLabel": "Contributors",
"contributorsHint": "A list of contributors of ReVanced",
"logsLabel": "Logs",
"logsHint": "Share Manager's logs",
"logsLabel": "Share logs",
"logsHint": "Share ReVanced Manager logs",
"enablePatchesSelectionLabel": "Enable changing selection",
"enablePatchesSelectionHint": "Enable changing the selection of patches.",
"enablePatchesSelectionWarningText": "Changing the default selection of patches may cause unexpected issues.\n\nEnable anyways?",
"enablePatchesSelectionLabel": "Allow changing patch selection",
"enablePatchesSelectionHint": "Allow changing the selection of patches",
"enablePatchesSelectionWarningText": "Changing the selection of patches may cause unexpected issues.\n\nEnable anyways?",
"disablePatchesSelectionWarningText": "You are about to disable changing the selection of patches.\nThe default selection of patches will be restored.\n\nDisable anyways?",
"autoUpdatePatchesLabel": "Auto update patches",
"autoUpdatePatchesHint": "Automatically update ReVanced Patches to the latest version",
"experimentalUniversalPatchesLabel": "Experimental universal patches support",
"experimentalUniversalPatchesHint": "Display all applications to use with universal patches, loading list of apps may be slower",
"experimentalPatchesLabel": "Experimental patches support",
"experimentalPatchesHint": "Enable usage of unsupported patches in any app version",
"enabledExperimentalPatches": "Experimental patches support enabled",
"autoUpdatePatchesHint": "Automatically update patches to the latest version",
"universalPatchesLabel": "Show universal patches",
"universalPatchesHint": "Display all apps and universal patches (may slow down the app list)",
"versionCompatibilityCheckLabel": "Version compatibility check",
"versionCompatibilityCheckHint": "Restricts patches to supported versions",
"aboutLabel": "About",
"snackbarMessage": "Copied to clipboard",
@ -224,47 +242,52 @@
"deleteTempDirHint": "Delete unused temporary files",
"deletedTempDir": "Temporary files deleted",
"exportPatchesLabel": "Export patches selection",
"exportPatchesHint": "Export patches selection to a JSON file",
"exportedPatches": "Patches selection exported",
"noExportFileFound": "No patches selection to export",
"exportPatchesLabel": "Export patch selection",
"exportPatchesHint": "Export patch selection to a JSON file",
"exportedPatches": "Patch selection exported",
"noExportFileFound": "No patch selection to export",
"importPatchesLabel": "Import patches selection",
"importPatchesHint": "Import patches selection from a JSON file",
"importedPatches": "Patches selection imported",
"importPatchesLabel": "Import patch selection",
"importPatchesHint": "Import patch selection from a JSON file",
"importedPatches": "Patch selection imported",
"resetStoredPatchesLabel": "Reset patches",
"resetStoredPatchesHint": "Reset the stored patches selection",
"resetStoredPatchesLabel": "Reset patch selection",
"resetStoredPatchesHint": "Reset the stored patch selection",
"resetStoredPatchesDialogTitle": "Reset patch selection?",
"resetStoredPatchesDialogText": "The default selection of patches will be restored.",
"resetStoredPatches": "Patch selection has been reset",
"resetStoredPatchesDialogTitle": "Reset patches selection?",
"resetStoredPatchesDialogText": "Resetting patches selection will remove all selected patches.",
"resetStoredPatches": "Patches selection has been reset",
"resetStoredOptionsLabel": "Reset patch options",
"resetStoredOptionsHint": "Reset all patch options",
"resetStoredOptionsDialogTitle": "Reset patch options?",
"resetStoredOptionsDialogText": "Resetting patch options will remove all saved options.",
"resetStoredOptions": "Options have been reset",
"deleteLogsLabel": "Delete logs",
"deleteLogsHint": "Delete collected manager logs",
"deleteLogsLabel": "Clear logs",
"deleteLogsHint": "Delete collected ReVanced Manager logs",
"deletedLogs": "Logs deleted",
"regenerateKeystoreLabel": "Regenerate keystore",
"regenerateKeystoreHint": "Regenerate the keystore used to sign the app",
"regenerateKeystoreHint": "Regenerate the keystore used to sign apps",
"regenerateKeystoreDialogTitle": "Regenerate keystore?",
"regenerateKeystoreDialogText": "Patched apps signed with the old keystore will no longer be able to update.",
"regenerateKeystoreDialogText": "Patched apps signed with the old keystore will no longer be able to be updated.",
"regeneratedKeystore": "Keystore regenerated",
"exportKeystoreLabel": "Export keystore",
"exportKeystoreHint": "Export keystore used to sign apps",
"exportKeystoreHint": "Export the keystore used to sign apps",
"exportedKeystore": "Keystore exported",
"noKeystoreExportFileFound": "No keystore to export",
"importKeystoreLabel": "Import keystore",
"importKeystoreHint": "Import keystore used to sign apps",
"importKeystoreHint": "Import a keystore used to sign apps",
"importedKeystore": "Keystore imported",
"selectKeystorePassword": "Keystore Password",
"selectKeystorePasswordHint": "Select keystore password used to sign the apk",
"selectKeystorePassword": "Keystore password",
"selectKeystorePasswordHint": "Select keystore password used to sign apps",
"jsonSelectorErrorMessage": "Unable to use selected JSON file",
"keystoreSelectorErrorMessage": "Unable to use selected KEYSTORE file"
"keystoreSelectorErrorMessage": "Unable to use selected keystore file"
},
"appInfoView": {
"widgetTitle": "App info",

View File

@ -1,9 +1,4 @@
project_id_env: CROWDIN_PROJECT_ID
api_token_env: CROWDIN_PERSONAL_TOKEN
commit_message: 'chore(i18n): sync translations'
preserve_hierarchy: true
preserve_hierarchy: 1
files:
- source: assets/i18n/en_US.json
translation: assets/i18n/%locale_with_underscore%.json
- source: /assets/i18n/en_US.json
translation: /assets/i18n/%locale_with_underscore%.json

View File

@ -7,16 +7,27 @@ The following pages will guide you through using ReVanced Manager to patch apps.
1. Navigate to the **Patcher** tab from the bottom navigation bar
2. Tap on the **Select an app** card
3. Choose an app to patch[^1]
> **Note**: The suggested version is visible in each app's card.
4. Tap on the **Select patches** card and select the patches you want to apply[^2]
> **Warning**: If you see a warning you can click on it for more information.
5. Tap on the **Done** then **Patch** button
> **Warning**: The patching process may take ~5 minutes. Exiting the app may increase the time it takes to patch.
6. Tap on the **Install** button
> **Note**: If you are rooted, you can mount the patched app on top of the original app.[^3]
> Optionally, you may export the patched app to storage using the options in the top right corner.
[^1]: Non-root users may be prompted to select an APK from storage, in which case you have to source the APK file yourself. ReVanced does not provide any APK files.
> [!NOTE]
> The suggested version is visible in each app's card.
4. Tap on the **Select patches** card and select the patches you want to apply[^2].
> [!NOTE]
> Some patches have options that can or must be configured by tapping on ⚙️ icon next to the patch name.
>[!WARNING]
> If you see a warning you can click on it for more information.
5. Tap on the **Done** then **Patch** button
> [!WARNING]
> The patching process may take ~5 minutes. Exiting the app may cancel patching or increase the time it takes to patch.
6. Tap on the **Install** button
> [!NOTE]
> If you are rooted, you can mount the patched app on top of the original app.[^3]
> Optionally, you may export the patched app to storage using the option in the bottom left corner.
[^1]: Non-root users may be prompted to select an APK from storage, in which case you must source the APK file yourself. ReVanced does not provide any APK files.
[^2]: It is suggested to use the default set of patches by tapping on the **Default** button above the list of patches.
[^3]: Mounting the patched app on top of the original app will only work if the installed app version matches the version of the app selected in step 3. above.

View File

@ -2,33 +2,46 @@
ReVanced Manager has settings that can be configured to your liking.
## Essential settings
## 🎛️ Essential settings
- ### 🔗 API URL
- ### 🪛 Allow changing patch selection
Specify the URL of the API to use. This is used to fetch ReVanced Patches and update ReVanced Manager.
Allows the user to change the patch selection from the default selection.
- ### 🔍 Version compatibility check
Constrains patches to supported app versions. Disable this to patch any version of an app.
> [!WARNING]
> Disabling this may cause issues if the patches are not compatible with the app version.
- ### 🧑‍🔬 Show universal patches
Reveals patches which can be applied to any app.
> [!WARNING]
> These patches may not work on all apps.
- ### 🧬 Sources
Override the API and change the source of ReVanced Patches.
Override the API and download patches from a different source.
- ### 🧪 Experimental ReVanced Patches support
- ### 🔗 API URL
Lift app version constraints from ReVanced Patches. This allows you to patch any version of an app, even if the patch is not explicitly compatible with it.
API to use to fetch updates and patches from.
- ### 🧑‍🔬 Experimental universal support
- ### 💾 Imports & Exports
This will show or hide ReVanced Patches, which are not meant for any app in particular but rather for all apps but may not work on all apps.
You can import, export or reset the following settings:
- ### 🔑 Export, import or delete keystore
- 🔑 Keystore
- 📄 Patch selection
- ⚙️ Patch options
Manage the keystore used to sign patched apps.
> [!NOTE]
> This is particularly useful if you want to backup or reset your settings.
- ### 📄 Export, import or reset ReVanced Patches selection
Manage the ReVanced Patches selection. This is useful if you want to share your ReVanced Patches selection with others or reset it to the default selection.
- ### About
- ### ❓ About
View information about your device and ReVanced Manager. This includes the version of ReVanced Manager and supported architectures of your device.
@ -36,4 +49,4 @@ ReVanced Manager has settings that can be configured to your liking.
The next page will bring you back to the usage page.
Continue: [🛠️ Usage](2_usage.md)
Continue: [🛠️ Usage](2_usage.md)

View File

@ -6,7 +6,7 @@ In case you encounter any issues while using ReVanced Manager, please refer to t
Make sure ReVanced Manager is up to date by following [🔄 Updating ReVanced Manager](2_3_updating.md) and select the **Default** button when choosing patches.
- 💥 App not installed as package conflicts with an existing package
- 🚫 App not installed as package conflicts with an existing package
An existing installation of the app you're trying to patch is conflicting with the patched app. Uninstall the existing app before installing the patched app.
@ -16,10 +16,6 @@ In case you encounter any issues while using ReVanced Manager, please refer to t
Alternatively, you can use [ReVanced CLI](https://github.com/revanced/revanced-cli) to patch the app.
- 🚫 Non-root install is not possible with the current patches selection
Select the **Default** button when choosing patches.
- 🚨 Patched app crashes on launch
Select the **Default** button when choosing patches.

View File

@ -31,7 +31,8 @@ This page will guide you through building ReVanced Manager from source.
flutter packages pub run build_runner build --delete-conflicting-outputs
```
> **Note**: Must be run every time you sync your local repository with the remote repository.
> [!Note]
> Must be run every time you sync your local repository with the remote repository.
7. Build the APK

BIN
fonts/custom-icons.ttf Normal file

Binary file not shown.

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View File

@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

240
gradlew vendored Normal file
View File

@ -0,0 +1,240 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

91
gradlew.bat vendored Normal file
View File

@ -0,0 +1,91 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@ -1,3 +1,4 @@
import 'package:revanced_manager/services/download_manager.dart';
import 'package:revanced_manager/services/github_api.dart';
import 'package:revanced_manager/services/manager_api.dart';
import 'package:revanced_manager/services/patcher_api.dart';
@ -9,6 +10,8 @@ import 'package:revanced_manager/ui/views/home/home_viewmodel.dart';
import 'package:revanced_manager/ui/views/installer/installer_view.dart';
import 'package:revanced_manager/ui/views/navigation/navigation_view.dart';
import 'package:revanced_manager/ui/views/navigation/navigation_viewmodel.dart';
import 'package:revanced_manager/ui/views/patch_options/patch_options_view.dart';
import 'package:revanced_manager/ui/views/patch_options/patch_options_viewmodel.dart';
import 'package:revanced_manager/ui/views/patcher/patcher_view.dart';
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
import 'package:revanced_manager/ui/views/patches_selector/patches_selector_view.dart';
@ -23,6 +26,7 @@ import 'package:stacked_services/stacked_services.dart';
MaterialRoute(page: PatcherView),
MaterialRoute(page: AppSelectorView),
MaterialRoute(page: PatchesSelectorView),
MaterialRoute(page: PatchOptionsView),
MaterialRoute(page: InstallerView),
MaterialRoute(page: SettingsView),
MaterialRoute(page: ContributorsView),
@ -32,11 +36,13 @@ import 'package:stacked_services/stacked_services.dart';
LazySingleton(classType: NavigationViewModel),
LazySingleton(classType: HomeViewModel),
LazySingleton(classType: PatcherViewModel),
LazySingleton(classType: PatchOptionsViewModel),
LazySingleton(classType: NavigationService),
LazySingleton(classType: ManagerAPI),
LazySingleton(classType: PatcherAPI),
LazySingleton(classType: RevancedAPI),
LazySingleton(classType: GithubAPI),
LazySingleton(classType: DownloadManager),
LazySingleton(classType: Toast),
],
)

View File

@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/services/download_manager.dart';
import 'package:revanced_manager/services/github_api.dart';
import 'package:revanced_manager/services/manager_api.dart';
import 'package:revanced_manager/services/revanced_api.dart';
@ -19,6 +20,7 @@ Future main() async {
await setupLocator();
WidgetsFlutterBinding.ensureInitialized();
await locator<ManagerAPI>().initialize();
await locator<DownloadManager>().initialize();
final String apiUrl = locator<ManagerAPI>().getApiUrl();
await locator<RevancedAPI>().initialize(apiUrl);
final String repoUrl = locator<ManagerAPI>().getRepoUrl();

View File

@ -9,13 +9,23 @@ class Patch {
required this.description,
required this.excluded,
required this.compatiblePackages,
required this.options,
});
factory Patch.fromJson(Map<String, dynamic> json) => _$PatchFromJson(json);
factory Patch.fromJson(Map<String, dynamic> json) {
// See: https://github.com/ReVanced/revanced-manager/issues/1364#issuecomment-1760414618
if (json['options'] == null) {
json['options'] = [];
}
return _$PatchFromJson(json);
}
final String name;
final String? description;
final bool excluded;
final List<Package> compatiblePackages;
final List<Option> options;
Map<String, dynamic> toJson() => _$PatchToJson(this);
@ -33,8 +43,32 @@ class Package {
factory Package.fromJson(Map<String, dynamic> json) =>
_$PackageFromJson(json);
final String name;
final List<String> versions;
Map toJson() => _$PackageToJson(this);
}
@JsonSerializable()
class Option {
Option({
required this.key,
required this.title,
required this.description,
required this.value,
required this.required,
required this.optionClassType,
});
factory Option.fromJson(Map<String, dynamic> json) => _$OptionFromJson(json);
final String key;
final String title;
final String description;
dynamic value;
final bool required;
final String optionClassType;
Map toJson() => _$OptionToJson(this);
}

View File

@ -0,0 +1,75 @@
import 'package:dio/dio.dart';
import 'package:dio_cache_interceptor/dio_cache_interceptor.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_cache_manager/file.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:injectable/injectable.dart';
import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/services/manager_api.dart';
@lazySingleton
class DownloadManager {
final ManagerAPI _managerAPI = locator<ManagerAPI>();
late final String _userAgent;
final _cacheOptions = CacheOptions(
store: MemCacheStore(),
maxStale: const Duration(days: 1),
priority: CachePriority.high,
);
Future<void> initialize() async {
_userAgent = 'ReVanced-Manager/${await _managerAPI.getCurrentManagerVersion()}';
}
Dio initDio(String url) {
var dio = Dio();
try {
dio = Dio(
BaseOptions(
baseUrl: url,
headers: {
'User-Agent': _userAgent,
},
),
);
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
}
dio.interceptors.add(DioCacheInterceptor(options: _cacheOptions));
return dio;
}
Future<void> clearAllCache() async {
try {
await _cacheOptions.store!.clean();
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
}
}
Future<File> getSingleFile(String url) async {
return DefaultCacheManager().getSingleFile(
url,
headers: {
'User-Agent': _userAgent,
},
);
}
Stream<FileResponse> getFileStream(String url) {
return DefaultCacheManager().getFileStream(
url,
withProgress: true,
headers: {
'User-Agent': _userAgent,
},
);
}
}

View File

@ -1,48 +1,24 @@
import 'dart:io';
import 'package:collection/collection.dart';
import 'package:dio/dio.dart';
import 'package:dio_cache_interceptor/dio_cache_interceptor.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:injectable/injectable.dart';
import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/services/download_manager.dart';
import 'package:revanced_manager/services/manager_api.dart';
@lazySingleton
class GithubAPI {
late Dio _dio = Dio();
late final Dio _dio;
late final ManagerAPI _managerAPI = locator<ManagerAPI>();
final _cacheOptions = CacheOptions(
store: MemCacheStore(),
maxStale: const Duration(days: 1),
priority: CachePriority.high,
);
late final DownloadManager _downloadManager = locator<DownloadManager>();
Future<void> initialize(String repoUrl) async {
try {
_dio = Dio(
BaseOptions(
baseUrl: repoUrl,
),
);
_dio.interceptors.add(DioCacheInterceptor(options: _cacheOptions));
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
}
_dio = _downloadManager.initDio(repoUrl);
}
Future<void> clearAllCache() async {
try {
await _cacheOptions.store!.clean();
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
}
await _downloadManager.clearAllCache();
}
Future<Map<String, dynamic>?> getLatestRelease(
@ -104,7 +80,7 @@ class GithubAPI {
final Map<String, dynamic> releases = response.data[0];
int updates = 0;
final String currentVersion =
await ManagerAPI().getCurrentManagerVersion();
await _managerAPI.getCurrentManagerVersion();
while (response.data[updates]['tag_name'] != 'v$currentVersion') {
updates++;
}
@ -141,7 +117,7 @@ class GithubAPI {
(asset) => (asset['name'] as String).endsWith(extension),
);
if (asset != null) {
return await DefaultCacheManager().getSingleFile(
return await _downloadManager.getSingleFile(
asset['browser_download_url'],
);
}
@ -162,7 +138,7 @@ class GithubAPI {
) async {
try {
if (url.isNotEmpty) {
return await DefaultCacheManager().getSingleFile(
return await _downloadManager.getSingleFile(
url,
);
}
@ -180,7 +156,7 @@ class GithubAPI {
} else {
_managerAPI.setPatchesDownloadURL(downloadUrl);
}
return await DefaultCacheManager().getSingleFile(
return await _downloadManager.getSingleFile(
downloadUrl,
);
}

View File

@ -28,6 +28,10 @@ class ManagerAPI {
final String cliRepo = 'revanced-cli';
late SharedPreferences _prefs;
List<Patch> patches = [];
List<Option> modifiedOptions = [];
List<Option> options = [];
Patch? selectedPatch;
BuildContext? ctx;
bool isRooted = false;
String storedPatchesFile = '/selected-patches.json';
String keystoreFile =
@ -182,6 +186,29 @@ class ManagerAPI {
await _prefs.setStringList('usedPatches-$packageName', patchesJson);
}
Option? getPatchOption(String packageName, String patchName, String key) {
final String? optionJson =
_prefs.getString('patchOption-$packageName-$patchName-$key');
if (optionJson != null) {
final Option option = Option.fromJson(jsonDecode(optionJson));
return option;
} else {
return null;
}
}
void setPatchOption(Option option, String patchName, String packageName) {
final String optionJson = jsonEncode(option.toJson());
_prefs.setString(
'patchOption-$packageName-$patchName-${option.key}',
optionJson,
);
}
void clearPatchOption(String packageName, String patchName, String key) {
_prefs.remove('patchOption-$packageName-$patchName-$key');
}
String getIntegrationsRepo() {
return _prefs.getString('integrationsRepo') ?? defaultIntegrationsRepo;
}
@ -217,12 +244,12 @@ class ManagerAPI {
await _prefs.setBool('universalPatchesEnabled', value);
}
bool areExperimentalPatchesEnabled() {
return _prefs.getBool('experimentalPatchesEnabled') ?? false;
bool isVersionCompatibilityCheckEnabled() {
return _prefs.getBool('versionCompatibilityCheckEnabled') ?? true;
}
Future<void> enableExperimentalPatchesStatus(bool value) async {
await _prefs.setBool('experimentalPatchesEnabled', value);
Future<void> enableVersionCompatibilityCheckStatus(bool value) async {
await _prefs.setBool('versionCompatibilityCheckEnabled', value);
}
Future<void> setKeystorePassword(String password) async {
@ -314,7 +341,6 @@ class ManagerAPI {
Directory('${appCache.path}/cache').createTempSync('tmp-');
final Directory cacheDir = Directory('${workDir.path}/cache');
cacheDir.createSync();
if (patchBundleFile != null) {
try {
final String patchesJson = await PatcherAPI.patcherChannel.invokeMethod(
@ -324,7 +350,6 @@ class ManagerAPI {
'cacheDirPath': cacheDir.path,
},
);
final List<dynamic> patchesJsonList = jsonDecode(patchesJson);
patches = patchesJsonList
.map((patchJson) => Patch.fromJson(patchJson))
@ -336,6 +361,7 @@ class ManagerAPI {
}
}
}
return List.empty();
}
@ -596,7 +622,7 @@ class ManagerAPI {
// Remove apps that are not installed anymore.
final List<PatchedApplication> toRemove =
await getAppsToRemove(patchedApps);
await getAppsToRemove(patchedApps);
patchedApps.removeWhere((a) => toRemove.contains(a));
// Determine all apps that are installed by mounting.
@ -651,7 +677,7 @@ class ManagerAPI {
Future<List<String>> getDefaultPatches() async {
final List<Patch> patches = await getPatches();
final List<String> defaultPatches = [];
if (areExperimentalPatchesEnabled() == false) {
if (isVersionCompatibilityCheckEnabled() == true) {
defaultPatches.addAll(
patches
.where(
@ -688,6 +714,14 @@ class ManagerAPI {
return jsonDecode(string);
}
void resetAllOptions() {
_prefs.getKeys().where((key) => key.startsWith('patchOption-')).forEach(
(key) {
_prefs.remove(key);
},
);
}
Future<void> resetLastSelectedPatches() async {
final File selectedPatchesFile = File(storedPatchesFile);
if (selectedPatchesFile.existsSync()) {

View File

@ -18,7 +18,7 @@ import 'package:share_extend/share_extend.dart';
@lazySingleton
class PatcherAPI {
static const patcherChannel =
MethodChannel('app.revanced.manager.flutter/patcher');
MethodChannel('app.revanced.manager.flutter/patcher');
final ManagerAPI _managerAPI = locator<ManagerAPI>();
final RootAPI _rootAPI = RootAPI();
late Directory _dataDir;
@ -79,8 +79,7 @@ class PatcherAPI {
}
Future<List<ApplicationWithIcon>> getFilteredInstalledApps(
bool showUniversalPatches,
) async {
bool showUniversalPatches,) async {
final List<ApplicationWithIcon> filteredApps = [];
final bool allAppsIncluded =
_universalPatches.isNotEmpty && showUniversalPatches;
@ -122,11 +121,11 @@ class PatcherAPI {
final List<Patch> patches = _patches
.where(
(patch) =>
patch.compatiblePackages.isEmpty ||
!patch.name.contains('settings') &&
patch.compatiblePackages
.any((pack) => pack.name == packageName),
)
patch.compatiblePackages.isEmpty ||
!patch.name.contains('settings') &&
patch.compatiblePackages
.any((pack) => pack.name == packageName),
)
.toList();
if (!_managerAPI.areUniversalPatchesEnabled()) {
filteredPatches[packageName] = patches
@ -138,20 +137,29 @@ class PatcherAPI {
return filteredPatches[packageName];
}
Future<List<Patch>> getAppliedPatches(
List<String> appliedPatches,
) async {
Future<List<Patch>> getAppliedPatches(List<String> appliedPatches,) async {
return _patches
.where((patch) => appliedPatches.contains(patch.name))
.toList();
}
Future<void> runPatcher(
String packageName,
String apkFilePath,
List<Patch> selectedPatches,
) async {
Future<void> runPatcher(String packageName,
String apkFilePath,
List<Patch> selectedPatches,) async {
final File? integrationsFile = await _managerAPI.downloadIntegrations();
final Map<String, Map<String, dynamic>> options = {};
for (final patch in selectedPatches) {
if (patch.options.isNotEmpty) {
final Map<String, dynamic> patchOptions = {};
for (final option in patch.options) {
final patchOption = _managerAPI.getPatchOption(packageName, patch.name, option.key);
if (patchOption != null) {
patchOptions[patchOption.key] = patchOption.value;
}
}
options[patch.name] = patchOptions;
}
}
if (integrationsFile != null) {
_dataDir.createSync();
@ -163,6 +171,7 @@ class PatcherAPI {
final Directory cacheDir = Directory('${workDir.path}/cache');
cacheDir.createSync();
final String originalFilePath = apkFilePath;
try {
await patcherChannel.invokeMethod(
'runPatcher',
@ -173,6 +182,7 @@ class PatcherAPI {
'outFilePath': outFile!.path,
'integrationsPath': integrationsFile.path,
'selectedPatches': selectedPatches.map((p) => p.name).toList(),
'options': options,
'cacheDirPath': cacheDir.path,
'keyStoreFilePath': _keyStoreFile.path,
'keystorePassword': _managerAPI.getKeystorePassword(),
@ -184,131 +194,131 @@ class PatcherAPI {
}
}
}
}
}
Future<void> stopPatcher() async {
try {
await patcherChannel.invokeMethod('stopPatcher');
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
Future<void> stopPatcher() async {
try {
await patcherChannel.invokeMethod('stopPatcher');
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
}
Future<bool> installPatchedFile(PatchedApplication patchedApp) async {
if (outFile != null) {
try {
if (patchedApp.isRooted) {
final bool hasRootPermissions = await _rootAPI.hasRootPermissions();
if (hasRootPermissions) {
return _rootAPI.installApp(
patchedApp.packageName,
patchedApp.apkFilePath,
outFile!.path,
);
}
} else {
final install = await InstallPlugin.installApk(outFile!.path);
return install['isSuccess'];
}
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
return false;
}
}
return false;
}
void exportPatchedFile(String appName, String version) {
try {
if (outFile != null) {
final String newName = _getFileName(appName, version);
CRFileSaver.saveFileWithDialog(
SaveFileDialogParams(
sourceFilePath: outFile!.path,
destinationFileName: newName,
),
);
}
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
}
}
void sharePatchedFile(String appName, String version) {
try {
if (outFile != null) {
final String newName = _getFileName(appName, version);
final int lastSeparator = outFile!.path.lastIndexOf('/');
final String newPath =
outFile!.path.substring(0, lastSeparator + 1) + newName;
final File shareFile = outFile!.copySync(newPath);
ShareExtend.share(shareFile.path, 'file');
}
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
}
}
String _getFileName(String appName, String version) {
final String prefix = appName.toLowerCase().replaceAll(' ', '-');
final String newName = '$prefix-revanced_v$version.apk';
return newName;
}
Future<void> exportPatcherLog(String logs) async {
final Directory appCache = await getTemporaryDirectory();
final Directory logDir = Directory('${appCache.path}/logs');
logDir.createSync();
final String dateTime = DateTime.now()
.toIso8601String()
.replaceAll('-', '')
.replaceAll(':', '')
.replaceAll('T', '')
.replaceAll('.', '');
final String fileName = 'revanced-manager_patcher_$dateTime.log';
final File log = File('${logDir.path}/$fileName');
log.writeAsStringSync(logs);
CRFileSaver.saveFileWithDialog(
SaveFileDialogParams(
sourceFilePath: log.path,
destinationFileName: fileName,
),
);
}
String getSuggestedVersion(String packageName) {
final Map<String, int> versions = {};
for (final Patch patch in _patches) {
final Package? package = patch.compatiblePackages.firstWhereOrNull(
(pack) => pack.name == packageName,
);
if (package != null) {
for (final String version in package.versions) {
versions.update(
version,
(value) => versions[version]! + 1,
ifAbsent: () => 1,
);
}
}
}
if (versions.isNotEmpty) {
final entries = versions.entries.toList()
..sort((a, b) => a.value.compareTo(b.value));
versions
..clear()
..addEntries(entries);
versions.removeWhere((key, value) => value != versions.values.last);
return (versions.keys.toList()..sort()).last;
}
return '';
}
}
Future<bool> installPatchedFile(PatchedApplication patchedApp) async {
if (outFile != null) {
try {
if (patchedApp.isRooted) {
final bool hasRootPermissions = await _rootAPI.hasRootPermissions();
if (hasRootPermissions) {
return _rootAPI.installApp(
patchedApp.packageName,
patchedApp.apkFilePath,
outFile!.path,
);
}
} else {
final install = await InstallPlugin.installApk(outFile!.path);
return install['isSuccess'];
}
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
return false;
}
}
return false;
}
void exportPatchedFile(String appName, String version) {
try {
if (outFile != null) {
final String newName = _getFileName(appName, version);
CRFileSaver.saveFileWithDialog(
SaveFileDialogParams(
sourceFilePath: outFile!.path,
destinationFileName: newName,
),
);
}
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
}
}
void sharePatchedFile(String appName, String version) {
try {
if (outFile != null) {
final String newName = _getFileName(appName, version);
final int lastSeparator = outFile!.path.lastIndexOf('/');
final String newPath =
outFile!.path.substring(0, lastSeparator + 1) + newName;
final File shareFile = outFile!.copySync(newPath);
ShareExtend.share(shareFile.path, 'file');
}
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
}
}
String _getFileName(String appName, String version) {
final String prefix = appName.toLowerCase().replaceAll(' ', '-');
final String newName = '$prefix-revanced_v$version.apk';
return newName;
}
Future<void> exportPatcherLog(String logs) async {
final Directory appCache = await getTemporaryDirectory();
final Directory logDir = Directory('${appCache.path}/logs');
logDir.createSync();
final String dateTime = DateTime.now()
.toIso8601String()
.replaceAll('-', '')
.replaceAll(':', '')
.replaceAll('T', '')
.replaceAll('.', '');
final String fileName = 'revanced-manager_patcher_$dateTime.txt';
final File log = File('${logDir.path}/$fileName');
log.writeAsStringSync(logs);
CRFileSaver.saveFileWithDialog(
SaveFileDialogParams(
sourceFilePath: log.path,
destinationFileName: fileName,
),
);
}
String getSuggestedVersion(String packageName) {
final Map<String, int> versions = {};
for (final Patch patch in _patches) {
final Package? package = patch.compatiblePackages.firstWhereOrNull(
(pack) => pack.name == packageName,
);
if (package != null) {
for (final String version in package.versions) {
versions.update(
version,
(value) => versions[version]! + 1,
ifAbsent: () => 1,
);
}
}
}
if (versions.isNotEmpty) {
final entries = versions.entries.toList()
..sort((a, b) => a.value.compareTo(b.value));
versions
..clear()
..addEntries(entries);
versions.removeWhere((key, value) => value != versions.values.last);
return (versions.keys.toList()
..sort()).last;
}
return '';
}}

View File

@ -3,49 +3,27 @@ import 'dart:io';
import 'package:collection/collection.dart';
import 'package:dio/dio.dart';
import 'package:dio_cache_interceptor/dio_cache_interceptor.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:injectable/injectable.dart';
import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/services/download_manager.dart';
import 'package:synchronized/synchronized.dart';
import 'package:timeago/timeago.dart';
@lazySingleton
class RevancedAPI {
late Dio _dio = Dio();
late final Dio _dio;
late final DownloadManager _downloadManager = locator<DownloadManager>();
final Lock getToolsLock = Lock();
final _cacheOptions = CacheOptions(
store: MemCacheStore(),
maxStale: const Duration(days: 1),
priority: CachePriority.high,
);
Future<void> initialize(String apiUrl) async {
try {
_dio = Dio(
BaseOptions(
baseUrl: apiUrl,
),
);
_dio.interceptors.add(DioCacheInterceptor(options: _cacheOptions));
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
}
Future<void> initialize(String repoUrl) async {
_dio = _downloadManager.initDio(repoUrl);
}
Future<void> clearAllCache() async {
try {
await _cacheOptions.store!.clean();
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
}
await _downloadManager.clearAllCache();
}
Future<Map<String, List<dynamic>>> getContributors() async {
@ -120,7 +98,7 @@ class RevancedAPI {
);
if (release != null) {
final String url = release['browser_download_url'];
return await DefaultCacheManager().getSingleFile(url);
return await _downloadManager.getSingleFile(url);
}
} on Exception catch (e) {
if (kDebugMode) {
@ -152,9 +130,8 @@ class RevancedAPI {
'revanced/revanced-manager',
);
File? outputFile;
await for (final result in DefaultCacheManager().getFileStream(
await for (final result in _downloadManager.getFileStream(
release!['browser_download_url'] as String,
withProgress: true,
)) {
if (result is DownloadProgress) {
final totalSize = result.totalSize ?? 10000000;

View File

@ -23,7 +23,6 @@ class _AppSelectorViewState extends State<AppSelectorView> {
onViewModelReady: (model) => model.initialize(),
viewModelBuilder: () => AppSelectorViewModel(),
builder: (context, model, child) => Scaffold(
resizeToAvoidBottomInset: false,
floatingActionButton: FloatingActionButton.extended(
label: I18nText('appSelectorView.storageButton'),
icon: const Icon(Icons.sd_storage),
@ -39,13 +38,11 @@ class _AppSelectorViewState extends State<AppSelectorView> {
floating: true,
title: I18nText(
'appSelectorView.viewTitle',
child: Text(
'',
style: TextStyle(
),
titleTextStyle: TextStyle(
fontSize: 22.0,
color: Theme.of(context).textTheme.titleLarge!.color,
),
),
),
leading: IconButton(
icon: Icon(
Icons.arrow_back,

View File

@ -13,6 +13,7 @@ import 'package:revanced_manager/services/patcher_api.dart';
import 'package:revanced_manager/services/toast.dart';
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
import 'package:revanced_manager/utils/check_for_supported_patch.dart';
import 'package:stacked/stacked.dart';
class AppSelectorViewModel extends BaseViewModel {
@ -78,7 +79,7 @@ class AppSelectorViewModel extends BaseViewModel {
icon: application.icon,
patchDate: DateTime.now(),
);
locator<PatcherViewModel>().loadLastSelectedPatches();
await locator<PatcherViewModel>().loadLastSelectedPatches();
}
Future<void> canSelectInstalled(
@ -93,10 +94,14 @@ class AppSelectorViewModel extends BaseViewModel {
return showSelectFromStorageDialog(context);
}
} else if (!await checkSplitApk(packageName) || isRooted) {
selectApp(app);
await selectApp(app);
if (context.mounted) {
Navigator.pop(context);
}
final List<Option> requiredNullOptions = getNullRequiredOptions(locator<PatcherViewModel>().selectedPatches, packageName);
if(requiredNullOptions.isNotEmpty){
locator<PatcherViewModel>().showRequiredOptionDialog();
}
}
}
}
@ -224,7 +229,8 @@ class AppSelectorViewModel extends BaseViewModel {
(app) =>
query.isEmpty ||
query.length < 2 ||
app.appName.toLowerCase().contains(query.toLowerCase()),
app.appName.toLowerCase().contains(query.toLowerCase()) ||
app.packageName.toLowerCase().contains(query.toLowerCase()),
)
.toList();
}

View File

@ -34,7 +34,6 @@ class HomeViewModel extends BaseViewModel {
final RevancedAPI _revancedAPI = locator<RevancedAPI>();
final Toast _toast = locator<Toast>();
final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
DateTime? _lastUpdate;
bool showUpdatableApps = false;
List<PatchedApplication> patchedInstalledApps = [];
String? _latestManagerVersion = '';

View File

@ -15,6 +15,8 @@ import 'package:revanced_manager/services/root_api.dart';
import 'package:revanced_manager/services/toast.dart';
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
import 'package:revanced_manager/utils/about_info.dart';
import 'package:screenshot_callback/screenshot_callback.dart';
import 'package:stacked/stacked.dart';
import 'package:wakelock/wakelock.dart';
@ -29,6 +31,7 @@ class InstallerViewModel extends BaseViewModel {
'app.revanced.manager.flutter/installer',
);
final ScrollController scrollController = ScrollController();
final ScreenshotCallback screenshotCallback = ScreenshotCallback();
double? progress = 0.0;
String logs = '';
String headerLogs = '';
@ -38,6 +41,7 @@ class InstallerViewModel extends BaseViewModel {
bool hasErrors = false;
bool isCanceled = false;
bool cancel = false;
bool showPopupScreenshotWarning = true;
Future<void> initialize(BuildContext context) async {
isRooted = await _rootAPI.isRooted();
@ -64,6 +68,12 @@ class InstallerViewModel extends BaseViewModel {
} // ignore
}
}
screenshotCallback.addListener(() {
if (showPopupScreenshotWarning) {
showPopupScreenshotWarning = false;
screenshotDetected(context);
}
});
await Wakelock.enable();
await handlePlatformChannelMethods();
await runPatcher();
@ -129,6 +139,7 @@ class InstallerViewModel extends BaseViewModel {
}
Future<void> runPatcher() async {
try {
await _patcherAPI.runPatcher(
_app.packageName,
@ -146,9 +157,9 @@ class InstallerViewModel extends BaseViewModel {
}
}
// Necessary to reset the state of patches by reloading them
// in a later patching process.
_managerAPI.patches.clear();
// Necessary to reset the state of patches so that they
// can be reloaded again.
_managerAPI.patches.clear();
await _patcherAPI.loadPatches();
try {
@ -169,6 +180,72 @@ class InstallerViewModel extends BaseViewModel {
}
}
Future<void> copyLogs() async {
final info = await AboutInfo.getInfo();
final formattedLogs = [
'```',
'~ Device Info',
'ReVanced Manager: ${info['version']}',
'Build: ${info['flavor']}',
'Model: ${info['model']}',
'Android version: ${info['androidVersion']}',
'Supported architectures: ${info['supportedArch'].join(", ")}',
'\n~ Patch Info',
'App: ${_app.packageName} v${_app.version}',
'Patches version: ${_managerAPI.patchesVersion}',
'Patches: ${_patches.map((p) => p.name).toList().join(", ")}',
'\n~ Settings',
'Allow changing patch selection: ${_managerAPI.isPatchesChangeEnabled()}',
'Show universal patches: ${_managerAPI.areUniversalPatchesEnabled()}',
'Version compatibility check: ${_managerAPI.isVersionCompatibilityCheckEnabled()}',
'Patches source: ${_managerAPI.getPatchesRepo()}',
'Integration source: ${_managerAPI.getIntegrationsRepo()}',
'\n~ Logs',
logs,
'```',
];
Clipboard.setData(ClipboardData(text: formattedLogs.join('\n')));
_toast.showBottom('installerView.copiedToClipboard');
}
Future<void> screenshotDetected(BuildContext context) async {
await showDialog(
context: context,
builder: (context) => AlertDialog(
title: I18nText(
'warning',
),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
icon: const Icon(Icons.warning),
content: SingleChildScrollView(
child: I18nText('installerView.screenshotDetected'),
),
actions: <Widget>[
CustomMaterialButton(
isFilled: false,
label: I18nText('noButton'),
onPressed: () {
Navigator.of(context).pop();
},
),
CustomMaterialButton(
label: I18nText('yesButton'),
onPressed: () {
copyLogs();
showPopupScreenshotWarning = true;
Navigator.of(context).pop();
},
),
],
),
);
}
Future<void> installTypeDialog(BuildContext context) async {
final ValueNotifier<int> installType = ValueNotifier(0);
if (isRooted) {
@ -316,10 +393,6 @@ class InstallerViewModel extends BaseViewModel {
}
}
void exportLog() {
_patcherAPI.exportPatcherLog(logs);
}
Future<void> cleanPatcher() async {
try {
_patcherAPI.cleanPatcher();
@ -343,7 +416,7 @@ class InstallerViewModel extends BaseViewModel {
exportResult();
break;
case 1:
exportLog();
copyLogs();
break;
}
}
@ -365,6 +438,7 @@ class InstallerViewModel extends BaseViewModel {
} else {
_patcherAPI.cleanPatcher();
}
screenshotCallback.dispose();
Navigator.of(context).pop();
return true;
}

View File

@ -19,7 +19,6 @@ class NavigationViewModel extends IndexTrackingViewModel {
Future<void> initialize(BuildContext context) async {
locator<Toast>().initialize(context);
final SharedPreferences prefs = await SharedPreferences.getInstance();
await requestManageExternalStorage();
if (prefs.getBool('permissionsRequested') == null) {
await Permission.storage.request();
@ -60,17 +59,6 @@ class NavigationViewModel extends IndexTrackingViewModel {
);
}
Future<void> requestManageExternalStorage() async {
final manageExternalStorageStatus =
await Permission.manageExternalStorage.status;
if (manageExternalStorageStatus.isDenied) {
await Permission.manageExternalStorage.request();
}
if (manageExternalStorageStatus.isPermanentlyDenied) {
await openAppSettings();
}
}
Widget getViewForIndex(int index) {
switch (index) {
case 0:

View File

@ -0,0 +1,129 @@
import 'package:flutter/material.dart';
import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:revanced_manager/models/patch.dart';
import 'package:revanced_manager/ui/views/patch_options/patch_options_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/patchesSelectorView/patch_options_fields.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
import 'package:stacked/stacked.dart';
class PatchOptionsView extends StatelessWidget {
const PatchOptionsView({super.key});
@override
Widget build(BuildContext context) {
return ViewModelBuilder<PatchOptionsViewModel>.reactive(
onViewModelReady: (model) => model.initialize(),
viewModelBuilder: () => PatchOptionsViewModel(),
builder: (context, model, child) => GestureDetector(
onTap: () => FocusScope.of(context).unfocus(),
child: Scaffold(
floatingActionButton: FloatingActionButton.extended(
onPressed: () async {
final bool saved = model.saveOptions(context);
if (saved && context.mounted) {
Navigator.pop(context);
}
},
label: I18nText('patchOptionsView.saveOptions'),
icon: const Icon(Icons.save),
),
body: CustomScrollView(
slivers: <Widget>[
SliverAppBar(
title: I18nText(
'patchOptionsView.viewTitle',
child: Text(
'',
style: GoogleFonts.inter(
color: Theme.of(context).textTheme.titleLarge!.color,
),
),
),
actions: [
IconButton(
onPressed: () {
model.resetOptions();
},
icon: const Icon(
Icons.history,
),
),
],
),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
for (final Option option in model.visibleOptions)
if (option.optionClassType == 'StringPatchOption' ||
option.optionClassType == 'IntPatchOption')
IntAndStringPatchOption(
patchOption: option,
removeOption: (option) {
model.removeOption(option);
},
onChanged: (value, option) {
model.modifyOptions(value, option);
},
)
else if (option.optionClassType == 'BooleanPatchOption')
BooleanPatchOption(
patchOption: option,
removeOption: (option) {
model.removeOption(option);
},
onChanged: (value, option) {
model.modifyOptions(value, option);
},
)
else if (option.optionClassType ==
'StringListPatchOption' ||
option.optionClassType == 'IntListPatchOption' ||
option.optionClassType == 'LongListPatchOption')
IntStringLongListPatchOption(
patchOption: option,
removeOption: (option) {
model.removeOption(option);
},
onChanged: (value, option) {
model.modifyOptions(value, option);
},
)
else
UnsupportedPatchOption(
patchOption: option,
),
if (model.visibleOptions.length !=
model.options.length) ...[
const SizedBox(
height: 8,
),
CustomMaterialButton(
onPressed: () {
model.showAddOptionDialog(context);
},
label: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.add),
I18nText('patchOptionsView.addOptions'),
],
),
),
],
const SizedBox(
height: 80,
),
],
),
),
),
],
),
),
),
);
}
}

View File

@ -0,0 +1,258 @@
import 'package:flutter/material.dart';
import 'package:flutter_i18n/widgets/I18nText.dart';
import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/models/patch.dart';
import 'package:revanced_manager/services/manager_api.dart';
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
import 'package:revanced_manager/ui/views/patches_selector/patches_selector_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
import 'package:stacked/stacked.dart';
class PatchOptionsViewModel extends BaseViewModel {
final ManagerAPI _managerAPI = locator<ManagerAPI>();
final String selectedApp =
locator<PatcherViewModel>().selectedApp!.packageName;
List<Option> options = [];
List<Option> savedOptions = [];
List<Option> visibleOptions = [];
Future<void> initialize() async {
options = getDefaultOptions();
for (final Option option in options) {
final Option? savedOption = _managerAPI.getPatchOption(
selectedApp,
_managerAPI.selectedPatch!.name,
option.key,
);
if (savedOption != null) {
savedOptions.add(savedOption);
}
}
if (savedOptions.isNotEmpty) {
visibleOptions = [
...savedOptions,
...options
.where(
(option) =>
option.required &&
!savedOptions.any((sOption) => sOption.key == option.key),
)
.toList(),
];
} else {
visibleOptions = [
...options.where((option) => option.required).toList(),
];
}
}
void addOption(Option option) {
visibleOptions.add(option);
notifyListeners();
}
void removeOption(Option option) {
visibleOptions.removeWhere((vOption) => vOption.key == option.key);
notifyListeners();
}
bool saveOptions(BuildContext context) {
final List<Option> requiredNullOptions = [];
for (final Option option in options) {
if (!visibleOptions.any((vOption) => vOption.key == option.key)) {
_managerAPI.clearPatchOption(
selectedApp, _managerAPI.selectedPatch!.name, option.key);
}
}
for (final Option option in visibleOptions) {
if (option.required && option.value == null) {
requiredNullOptions.add(option);
} else {
_managerAPI.setPatchOption(
option, _managerAPI.selectedPatch!.name, selectedApp);
}
}
if (requiredNullOptions.isNotEmpty) {
showRequiredOptionNullDialog(
context,
requiredNullOptions,
_managerAPI,
selectedApp,
);
return false;
}
return true;
}
void modifyOptions(dynamic value, Option option) {
final Option modifiedOption = Option(
title: option.title,
description: option.description,
optionClassType: option.optionClassType,
value: value,
required: option.required,
key: option.key,
);
visibleOptions[visibleOptions
.indexWhere((vOption) => vOption.key == option.key)] = modifiedOption;
_managerAPI.modifiedOptions
.removeWhere((mOption) => mOption.key == option.key);
_managerAPI.modifiedOptions.add(modifiedOption);
}
List<Option> getDefaultOptions() {
final List<Option> defaultOptions = [];
for (final option in _managerAPI.options) {
final Option defaultOption = Option(
title: option.title,
description: option.description,
optionClassType: option.optionClassType,
value: option.value is List ? option.value.toList() : option.value,
required: option.required,
key: option.key,
);
defaultOptions.add(defaultOption);
}
return defaultOptions;
}
void resetOptions() {
_managerAPI.modifiedOptions.clear();
visibleOptions =
getDefaultOptions().where((option) => option.required).toList();
notifyListeners();
}
Future<void> showAddOptionDialog(BuildContext context) async {
await showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
I18nText(
'patchOptionsView.addOptions',
),
Text(
'',
style: TextStyle(
fontSize: 16,
color: Theme.of(context).colorScheme.onSecondaryContainer,
),
),
],
),
actions: [
CustomMaterialButton(
label: I18nText('okButton'),
onPressed: () {
Navigator.of(context).pop();
},
),
],
contentPadding: const EdgeInsets.all(8),
content: Wrap(
spacing: 14,
runSpacing: 14,
children: options
.where(
(option) =>
!visibleOptions.any((vOption) => vOption.key == option.key),
)
.map((e) {
return CustomCard(
padding: const EdgeInsets.all(4),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
onTap: () {
addOption(e);
Navigator.pop(context);
},
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
e.title,
style: const TextStyle(
fontSize: 16,
),
),
const SizedBox(height: 4),
Text(
e.description,
style: TextStyle(
fontSize: 14,
color: Theme.of(context).colorScheme.onSecondaryContainer,
),
)
],
),
),
);
}).toList(),
),
),
);
}
}
Future<void> showRequiredOptionNullDialog(
BuildContext context,
List<Option> options,
ManagerAPI managerAPI,
String selectedApp,
) async {
final List<String> optionsTitles = [];
for (final option in options) {
optionsTitles.add('${option.title}');
}
await showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
title: I18nText('notice'),
actions: [
CustomMaterialButton(
isFilled: false,
label: I18nText(
'patchOptionsView.deselectPatch',
),
onPressed: () async {
if (managerAPI.isPatchesChangeEnabled()) {
locator<PatcherViewModel>()
.selectedPatches
.remove(managerAPI.selectedPatch);
locator<PatcherViewModel>().notifyListeners();
for (final option in options) {
managerAPI.clearPatchOption(
selectedApp, managerAPI.selectedPatch!.name, option.key);
}
Navigator.of(context)
..pop()
..pop()
..pop();
} else {
PatchesSelectorViewModel().showPatchesChangeDialog(context);
}
},
),
CustomMaterialButton(
label: I18nText('okButton'),
onPressed: () {
Navigator.of(context).pop();
},
),
],
content: I18nText(
'patchOptionsView.requiredOptionNull',
translationParams: {
'options': optionsTitles.join('\n'),
},
),
),
);
}

View File

@ -22,7 +22,14 @@ class PatcherView extends StatelessWidget {
child: FloatingActionButton.extended(
label: I18nText('patcherView.patchButton'),
icon: const Icon(Icons.build),
onPressed: () => model.showRemovedPatchesDialog(context),
onPressed: () async{
if (model.checkRequiredPatchOption(context)) {
final bool proceed = model.showRemovedPatchesDialog(context);
if (proceed && context.mounted) {
model.showArmv7WarningDialog(context);
}
}
},
),
),
body: CustomScrollView(
@ -45,7 +52,10 @@ class PatcherView extends StatelessWidget {
delegate: SliverChildListDelegate.fixed(
<Widget>[
AppSelectorCard(
onPressed: () => model.navigateToAppSelector(),
onPressed: () => {
model.navigateToAppSelector(),
model.ctx = context,
},
),
const SizedBox(height: 16),
Opacity(

View File

@ -21,6 +21,7 @@ class PatcherViewModel extends BaseViewModel {
final ManagerAPI _managerAPI = locator<ManagerAPI>();
final PatcherAPI _patcherAPI = locator<PatcherAPI>();
PatchedApplication? selectedApp;
BuildContext? ctx;
List<Patch> selectedPatches = [];
List<String> removedPatches = [];
@ -44,9 +45,9 @@ class PatcherViewModel extends BaseViewModel {
return selectedApp == null;
}
Future<void> showRemovedPatchesDialog(BuildContext context) async {
bool showRemovedPatchesDialog(BuildContext context) {
if (removedPatches.isNotEmpty) {
return showDialog(
showDialog(
context: context,
builder: (context) => AlertDialog(
title: I18nText('notice'),
@ -59,21 +60,59 @@ class PatcherViewModel extends BaseViewModel {
CustomMaterialButton(
isFilled: false,
label: I18nText('noButton'),
onPressed: () => Navigator.of(context).pop(),
onPressed: () {
Navigator.of(context).pop();
},
),
CustomMaterialButton(
label: I18nText('yesButton'),
onPressed: () {
Navigator.of(context).pop();
navigateToInstaller();
showArmv7WarningDialog(context);
},
),
],
),
);
} else {
showArmv7WarningDialog(context); // TODO(aabed): Find out why this is here
return false;
}
return true;
}
bool checkRequiredPatchOption(BuildContext context) {
if (getNullRequiredOptions(selectedPatches, selectedApp!.packageName)
.isNotEmpty) {
showRequiredOptionDialog(context);
return false;
}
return true;
}
void showRequiredOptionDialog([context]) {
showDialog(
context: context ?? ctx,
builder: (context) => AlertDialog(
title: I18nText('notice'),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
content: I18nText('patcherView.requiredOptionDialogText'),
actions: <Widget>[
CustomMaterialButton(
isFilled: false,
label: I18nText('cancelButton'),
onPressed: () => {
Navigator.of(context).pop(),
},
),
CustomMaterialButton(
label: I18nText('okButton'),
onPressed: () => {
Navigator.pop(context),
navigateToPatchesSelector(),
},
),
],
),
);
}
Future<void> showArmv7WarningDialog(BuildContext context) async {
@ -152,7 +191,7 @@ class PatcherViewModel extends BaseViewModel {
this.selectedPatches.clear();
this.selectedPatches.addAll(patches.where((patch) => !patch.excluded));
}
if (!_managerAPI.areExperimentalPatchesEnabled()) {
if (_managerAPI.isVersionCompatibilityCheckEnabled()) {
this.selectedPatches.removeWhere((patch) => !isPatchSupported(patch));
}
if (!_managerAPI.areUniversalPatchesEnabled()) {
@ -161,9 +200,13 @@ class PatcherViewModel extends BaseViewModel {
.removeWhere((patch) => patch.compatiblePackages.isEmpty);
}
final usedPatches = _managerAPI.getUsedPatches(selectedApp!.packageName);
for (final patch in usedPatches){
if (!patches.any((p) => p.name == patch.name)){
removedPatches.add('\u2022 ${patch.name}');
for (final patch in usedPatches) {
if (!patches.any((p) => p.name == patch.name)) {
removedPatches.add(' ${patch.name}');
for (final option in patch.options) {
_managerAPI.clearPatchOption(
selectedApp!.packageName, patch.name, option.key);
}
}
}
notifyListeners();

View File

@ -4,7 +4,6 @@ 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/custom_popup_menu.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';
@ -37,7 +36,6 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
onViewModelReady: (model) => model.initialize(),
viewModelBuilder: () => PatchesSelectorViewModel(),
builder: (context, model, child) => Scaffold(
resizeToAvoidBottomInset: false,
floatingActionButton: Visibility(
visible: model.patches.isNotEmpty,
child: FloatingActionButton.extended(
@ -49,8 +47,10 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
),
icon: const Icon(Icons.check),
onPressed: () {
model.selectPatches();
Navigator.of(context).pop();
if (!model.areRequiredOptionsNull(context)) {
model.selectPatches();
Navigator.of(context).pop();
}
},
),
),
@ -61,56 +61,50 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
floating: true,
title: I18nText(
'patchesSelectorView.viewTitle',
child: Text(
'',
style: TextStyle(
color: Theme.of(context).textTheme.titleLarge!.color,
),
),
),
titleTextStyle: TextStyle(
fontSize: 22.0,
color: Theme.of(context).textTheme.titleLarge!.color,
),
leading: IconButton(
icon: Icon(
Icons.arrow_back,
color: Theme.of(context).textTheme.titleLarge!.color,
),
onPressed: () => Navigator.of(context).pop(),
onPressed: () {
model.resetSelection();
Navigator.of(context).pop();
},
),
actions: [
FittedBox(
fit: BoxFit.scaleDown,
child: Container(
margin: const EdgeInsets.only(top: 12, bottom: 12),
padding:
const EdgeInsets.symmetric(horizontal: 6, vertical: 6),
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.tertiary
.withOpacity(0.5),
borderRadius: BorderRadius.circular(6),
),
child: Text(
model.patchesVersion!,
style: TextStyle(
color: Theme.of(context).textTheme.titleLarge!.color,
),
Container(
margin: const EdgeInsets.symmetric(vertical: 12),
padding: const EdgeInsets.symmetric(horizontal: 6),
decoration: BoxDecoration(
color:
Theme.of(context).colorScheme.tertiary.withOpacity(0.5),
borderRadius: BorderRadius.circular(6),
),
alignment: Alignment.center,
child: Text(
model.patchesVersion!,
style: TextStyle(
color: Theme.of(context).textTheme.titleLarge!.color,
),
),
),
CustomPopupMenu(
onSelected: (value) =>
{model.onMenuSelection(value, context)},
children: {
0: I18nText(
'patchesSelectorView.loadPatchesSelection',
child: const Text(
'',
style: TextStyle(
fontWeight: FontWeight.bold,
),
PopupMenuButton(
onSelected: (value) {
model.onMenuSelection(value, context);
},
itemBuilder: (BuildContext context) => <PopupMenuEntry>[
PopupMenuItem(
value: 0,
child: I18nText(
'patchesSelectorView.loadPatchesSelection',
),
),
},
],
),
],
bottom: PreferredSize(
@ -188,6 +182,93 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
),
],
),
if (model.newPatchExists())
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(
vertical: 10.0,
),
child: Container(
padding: const EdgeInsets.only(
top: 10.0,
bottom: 10.0,
left: 5.0,
),
child: I18nText(
'patchesSelectorView.newPatches',
child: Text(
'',
style: TextStyle(
color: Theme.of(context)
.colorScheme
.primary,
),
),
),
),
),
...model.getQueriedPatches(_query).map((patch) {
if (model.isPatchNew(patch)) {
return PatchItem(
name: patch.name,
simpleName: patch.getSimpleName(),
description: patch.description ?? '',
packageVersion:
model.getAppInfo().version,
supportedPackageVersions:
model.getSupportedVersions(patch),
isUnsupported: !isPatchSupported(patch),
isChangeEnabled:
_managerAPI.isPatchesChangeEnabled(),
hasUnsupportedPatchOption:
hasUnsupportedRequiredOption(
patch.options,
patch,
),
options: patch.options,
isSelected: model.isSelected(patch),
navigateToOptions: (options) =>
model.navigateToPatchOptions(
options,
patch,
),
onChanged: (value) => model.selectPatch(
patch,
value,
context,
),
);
} else {
return Container();
}
}),
Padding(
padding: const EdgeInsets.symmetric(
vertical: 10.0,
),
child: Container(
padding: const EdgeInsets.only(
top: 10.0,
bottom: 10.0,
left: 5.0,
),
child: I18nText(
'patchesSelectorView.patches',
child: Text(
'',
style: TextStyle(
color: Theme.of(context)
.colorScheme
.primary,
),
),
),
),
),
],
),
...model.getQueriedPatches(_query).map(
(patch) {
if (patch.compatiblePackages.isNotEmpty) {
@ -201,13 +282,23 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
isUnsupported: !isPatchSupported(patch),
isChangeEnabled:
_managerAPI.isPatchesChangeEnabled(),
isNew: model.isPatchNew(
hasUnsupportedPatchOption:
hasUnsupportedRequiredOption(
patch.options,
patch,
model.getAppInfo().packageName,
),
options: patch.options,
isSelected: model.isSelected(patch),
onChanged: (value) =>
model.selectPatch(patch, value, context),
navigateToOptions: (options) =>
model.navigateToPatchOptions(
options,
patch,
),
onChanged: (value) => model.selectPatch(
patch,
value,
context,
),
);
} else {
return Container();
@ -254,8 +345,18 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
isUnsupported: !isPatchSupported(patch),
isChangeEnabled:
_managerAPI.isPatchesChangeEnabled(),
isNew: false,
hasUnsupportedPatchOption:
hasUnsupportedRequiredOption(
patch.options,
patch,
),
options: patch.options,
isSelected: model.isSelected(patch),
navigateToOptions: (options) =>
model.navigateToPatchOptions(
options,
patch,
),
onChanged: (value) => model.selectPatch(
patch,
value,

View File

@ -2,6 +2,7 @@ import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter_i18n/widgets/I18nText.dart';
import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/app/app.router.dart';
import 'package:revanced_manager/models/patch.dart';
import 'package:revanced_manager/models/patched_application.dart';
import 'package:revanced_manager/services/manager_api.dart';
@ -11,11 +12,14 @@ import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
import 'package:revanced_manager/utils/check_for_supported_patch.dart';
import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart';
class PatchesSelectorViewModel extends BaseViewModel {
final PatcherAPI _patcherAPI = locator<PatcherAPI>();
final ManagerAPI _managerAPI = locator<ManagerAPI>();
final NavigationService _navigationService = locator<NavigationService>();
final List<Patch> patches = [];
final List<Patch> currentSelection = [];
final List<Patch> selectedPatches =
locator<PatcherViewModel>().selectedPatches;
PatchedApplication? selectedApp = locator<PatcherViewModel>().selectedApp;
@ -31,14 +35,18 @@ class PatchesSelectorViewModel extends BaseViewModel {
selectedApp!.packageName,
),
);
final List<Option> requiredNullOptions =
getNullRequiredOptions(patches, selectedApp!.packageName);
patches.sort((a, b) {
if (isPatchNew(a, selectedApp!.packageName) ==
isPatchNew(b, selectedApp!.packageName)) {
return a.name.compareTo(b.name);
if (b.options.any((option) => requiredNullOptions.contains(option)) &&
a.options.isEmpty) {
return 1;
} else {
return isPatchNew(b, selectedApp!.packageName) ? 1 : -1;
return a.name.compareTo(b.name);
}
});
currentSelection.clear();
currentSelection.addAll(selectedPatches);
notifyListeners();
}
@ -48,6 +56,60 @@ class PatchesSelectorViewModel extends BaseViewModel {
);
}
void navigateToPatchOptions(List<Option> setOptions, Patch patch) {
_managerAPI.options = setOptions;
_managerAPI.selectedPatch = patch;
_managerAPI.modifiedOptions.clear();
_navigationService.navigateToPatchOptionsView();
}
bool areRequiredOptionsNull(BuildContext context) {
final List<String> patchesWithNullRequiredOptions = [];
final List<Option> requiredNullOptions =
getNullRequiredOptions(selectedPatches, selectedApp!.packageName);
if (requiredNullOptions.isNotEmpty) {
for (final patch in selectedPatches) {
for (final patchOption in patch.options) {
if (requiredNullOptions.contains(patchOption)) {
patchesWithNullRequiredOptions.add(patch.name);
break;
}
}
}
showSetRequiredOption(context, patchesWithNullRequiredOptions);
return true;
}
return false;
}
Future<void> showSetRequiredOption(
BuildContext context,
List<String> patches,
) async {
return showDialog(
barrierDismissible: false,
context: context,
builder: (context) => AlertDialog(
title: I18nText('notice'),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
content: I18nText(
'patchesSelectorView.setRequiredOption',
translationParams: {
'patches': patches.map((patch) => '$patch').join('\n'),
},
),
actions: <Widget>[
CustomMaterialButton(
label: I18nText('okButton'),
onPressed: () => {
Navigator.of(context).pop(),
},
),
],
),
);
}
void selectPatch(Patch patch, bool isSelected, BuildContext context) {
if (_managerAPI.isPatchesChangeEnabled()) {
if (isSelected && !selectedPatches.contains(patch)) {
@ -107,7 +169,7 @@ class PatchesSelectorViewModel extends BaseViewModel {
.where(
(element) =>
!element.excluded &&
(_managerAPI.areExperimentalPatchesEnabled() ||
(!_managerAPI.isVersionCompatibilityCheckEnabled() ||
isPatchSupported(element)),
),
);
@ -123,9 +185,19 @@ class PatchesSelectorViewModel extends BaseViewModel {
void selectPatches() {
locator<PatcherViewModel>().selectedPatches = selectedPatches;
saveSelectedPatches();
if (_managerAPI.ctx != null) {
Navigator.pop(_managerAPI.ctx!);
_managerAPI.ctx = null;
}
locator<PatcherViewModel>().notifyListeners();
}
void resetSelection() {
selectedPatches.clear();
selectedPatches.addAll(currentSelection);
notifyListeners();
}
Future<void> getPatchesVersion() async {
patchesVersion = await _managerAPI.getCurrentPatchesVersion();
}
@ -137,7 +209,10 @@ class PatchesSelectorViewModel extends BaseViewModel {
query.isEmpty ||
query.length < 2 ||
patch.name.toLowerCase().contains(query.toLowerCase()) ||
patch.getSimpleName().toLowerCase().contains(query.toLowerCase()),
patch.name
.replaceAll(RegExp(r'[^\w\s]+'), '')
.toLowerCase()
.contains(query.toLowerCase()),
)
.toList();
if (_managerAPI.areUniversalPatchesEnabled()) {
@ -153,8 +228,9 @@ class PatchesSelectorViewModel extends BaseViewModel {
return locator<PatcherViewModel>().selectedApp!;
}
bool isPatchNew(Patch patch, String packageName) {
final List<Patch> savedPatches = _managerAPI.getSavedPatches(packageName);
bool isPatchNew(Patch patch) {
final List<Patch> savedPatches =
_managerAPI.getSavedPatches(selectedApp!.packageName);
if (savedPatches.isEmpty) {
return false;
} else {
@ -163,6 +239,12 @@ class PatchesSelectorViewModel extends BaseViewModel {
}
}
bool newPatchExists() {
return patches.any(
(patch) => isPatchNew(patch),
);
}
List<String> getSupportedVersions(Patch patch) {
final PatchedApplication app = locator<PatcherViewModel>().selectedApp!;
final Package? package = patch.compatiblePackages.firstWhereOrNull(
@ -202,7 +284,7 @@ class PatchesSelectorViewModel extends BaseViewModel {
this.selectedPatches.addAll(
patches.where((patch) => selectedPatches.contains(patch.name)),
);
if (!_managerAPI.areExperimentalPatchesEnabled()) {
if (_managerAPI.isVersionCompatibilityCheckEnabled()) {
this.selectedPatches.removeWhere((patch) => !isPatchSupported(patch));
}
} else {

View File

@ -6,8 +6,8 @@ import 'package:google_fonts/google_fonts.dart';
import 'package:revanced_manager/ui/views/settings/settingsFragment/settings_update_theme.dart';
import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_advanced_section.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_debug_section.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_export_section.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_info_section.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_team_section.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_sliver_app_bar.dart';
import 'package:stacked/stacked.dart';
@ -40,16 +40,24 @@ class SettingsView extends StatelessWidget {
SliverList(
delegate: SliverChildListDelegate.fixed(
<Widget>[
SUpdateThemeUI(),
// SUpdateLanguageUI(),
// _settingsDivider,
STeamSection(),
_settingsDivider,
SAdvancedSection(),
_settingsDivider,
SExportSection(),
_settingsDivider,
SInfoSection(),
ListView(
padding: EdgeInsets.zero,
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
children: const [
SUpdateThemeUI(),
// _settingsDivider,
// SUpdateLanguageUI(),
_settingsDivider,
SAdvancedSection(),
_settingsDivider,
SExportSection(),
_settingsDivider,
STeamSection(),
_settingsDivider,
SDebugSection(),
],
),
],
),
),

View File

@ -135,12 +135,12 @@ class SettingsViewModel extends BaseViewModel {
notifyListeners();
}
bool areExperimentalPatchesEnabled() {
return _managerAPI.areExperimentalPatchesEnabled();
bool isVersionCompatibilityCheckEnabled() {
return _managerAPI.isVersionCompatibilityCheckEnabled();
}
void useExperimentalPatches(bool value) {
_managerAPI.enableExperimentalPatchesStatus(value);
void useVersionCompatibilityCheck(bool value) {
_managerAPI.enableVersionCompatibilityCheckStatus(value);
notifyListeners();
}
@ -246,6 +246,11 @@ class SettingsViewModel extends BaseViewModel {
}
}
void resetAllOptions() {
_managerAPI.resetAllOptions();
_toast.showBottom('settingsView.resetStoredOptions');
}
void resetSelectedPatches() {
_managerAPI.resetLastSelectedPatches();
_toast.showBottom('settingsView.resetStoredPatches');

View File

@ -146,7 +146,7 @@ class AppInfoViewModel extends BaseViewModel {
}
String getAppliedPatchesString(List<String> appliedPatches) {
return '\u2022 ${appliedPatches.join('\n\u2022 ')}';
return ' ${appliedPatches.join('\n ')}';
}
void openApp(PatchedApplication app) {

View File

@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_cache_manager/file.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:revanced_manager/services/download_manager.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
import 'package:url_launcher/url_launcher.dart';
@ -58,7 +58,7 @@ class _ContributorsCardState extends State<ContributorsCard> {
mode: LaunchMode.externalApplication,
),
child: FutureBuilder<File?>(
future: DefaultCacheManager().getSingleFile(
future: DownloadManager().getSingleFile(
widget.contributors[index]['avatar_url'],
),
builder: (context, snapshot) => snapshot.hasData

View File

@ -75,7 +75,7 @@ class _LatestCommitCardState extends State<LatestCommitCard> {
const SizedBox(height: 16),
// ReVanced Patches
// Patches
CustomCard(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
@ -84,7 +84,7 @@ class _LatestCommitCardState extends State<LatestCommitCard> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
const Text('ReVanced Patches'),
const Text('Patches'),
const SizedBox(height: 4),
Row(
children: <Widget>[

View File

@ -58,8 +58,10 @@ class PatchSelectorCard extends StatelessWidget {
String _getPatchesSelection() {
String text = '';
for (final Patch p in locator<PatcherViewModel>().selectedPatches) {
text += '\u2022 ${p.getSimpleName()}\n';
final List<Patch> selectedPatches = locator<PatcherViewModel>().selectedPatches;
selectedPatches.sort((a, b) => a.name.compareTo(b.name));
for (final Patch p in selectedPatches) {
text += '${p.getSimpleName()}\n';
}
return text.substring(0, text.length - 1);
}

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/models/patch.dart';
import 'package:revanced_manager/services/manager_api.dart';
import 'package:revanced_manager/services/toast.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
@ -16,11 +17,12 @@ class PatchItem extends StatefulWidget {
required this.packageVersion,
required this.supportedPackageVersions,
required this.isUnsupported,
required this.isNew,
required this.hasUnsupportedPatchOption,
required this.options,
required this.isSelected,
required this.onChanged,
required this.navigateToOptions,
required this.isChangeEnabled,
this.child,
}) : super(key: key);
final String name;
final String simpleName;
@ -28,11 +30,12 @@ class PatchItem extends StatefulWidget {
final String packageVersion;
final List<String> supportedPackageVersions;
final bool isUnsupported;
final bool isNew;
final bool hasUnsupportedPatchOption;
final List<Option> options;
bool isSelected;
final Function(bool) onChanged;
final void Function(List<Option>) navigateToOptions;
final bool isChangeEnabled;
final Widget? child;
final toast = locator<Toast>();
final _managerAPI = locator<ManagerAPI>();
@ -45,157 +48,162 @@ class _PatchItemState extends State<PatchItem> {
Widget build(BuildContext context) {
widget.isSelected = widget.isSelected &&
(!widget.isUnsupported ||
widget._managerAPI.areExperimentalPatchesEnabled());
!widget._managerAPI.isVersionCompatibilityCheckEnabled()) &&
!widget.hasUnsupportedPatchOption;
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Opacity(
opacity: widget.isUnsupported &&
widget._managerAPI.areExperimentalPatchesEnabled() == false
widget._managerAPI.isVersionCompatibilityCheckEnabled() == true
? 0.5
: 1,
child: CustomCard(
padding: EdgeInsets.only(
top: 12,
bottom: 16,
left: 8.0,
right: widget.options.isNotEmpty ? 4.0 : 8.0,
),
onTap: () {
setState(() {
if (widget.isUnsupported &&
!widget._managerAPI.areExperimentalPatchesEnabled()) {
widget.isSelected = false;
widget.toast.showBottom('patchItem.unsupportedPatchVersion');
} else if (widget.isChangeEnabled) {
widget.isSelected = !widget.isSelected;
if (widget.isUnsupported &&
widget._managerAPI.isVersionCompatibilityCheckEnabled()) {
widget.isSelected = false;
widget.toast.showBottom('patchItem.unsupportedPatchVersion');
} else if (widget.isChangeEnabled) {
if (!widget.isSelected) {
if (widget.hasUnsupportedPatchOption) {
_showUnsupportedRequiredOptionDialog();
return;
}
}
});
if (!widget.isUnsupported || widget._managerAPI.areExperimentalPatchesEnabled()) {
widget.isSelected = !widget.isSelected;
setState(() {});
}
if (!widget.isUnsupported ||
!widget._managerAPI.isVersionCompatibilityCheckEnabled()) {
widget.onChanged(widget.isSelected);
}
},
child: Column(
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
Expanded(
child: Text(
widget.simpleName,
maxLines: 2,
overflow: TextOverflow.visible,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
],
),
const SizedBox(height: 4),
Text(
widget.description,
softWrap: true,
overflow: TextOverflow.visible,
style: TextStyle(
fontSize: 14,
color: Theme.of(context)
.colorScheme
.onSecondaryContainer,
),
),
],
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Transform.scale(
scale: 1.2,
child: Checkbox(
value: widget.isSelected,
activeColor: Theme.of(context).colorScheme.primary,
checkColor: Theme.of(context).colorScheme.secondaryContainer,
side: BorderSide(
width: 2.0,
color: Theme.of(context).colorScheme.primary,
),
Transform.scale(
scale: 1.2,
child: Checkbox(
value: widget.isSelected,
activeColor: Theme.of(context).colorScheme.primary,
checkColor:
Theme.of(context).colorScheme.secondaryContainer,
side: BorderSide(
width: 2.0,
color: Theme.of(context).colorScheme.primary,
),
onChanged: (newValue) {
setState(() {
if (widget.isUnsupported &&
!widget._managerAPI
.areExperimentalPatchesEnabled()) {
widget.isSelected = false;
widget.toast.showBottom(
'patchItem.unsupportedPatchVersion',
);
} else if (widget.isChangeEnabled) {
widget.isSelected = newValue!;
}
});
if (!widget.isUnsupported || widget._managerAPI.areExperimentalPatchesEnabled()) {
widget.onChanged(widget.isSelected);
onChanged: (newValue) {
if (widget.isUnsupported &&
widget._managerAPI
.isVersionCompatibilityCheckEnabled()) {
widget.isSelected = false;
widget.toast.showBottom(
'patchItem.unsupportedPatchVersion',
);
} else if (widget.isChangeEnabled) {
if (!widget.isSelected) {
if (widget.hasUnsupportedPatchOption) {
_showUnsupportedRequiredOptionDialog();
return;
}
},
),
}
widget.isSelected = newValue!;
setState(() {});
}
if (!widget.isUnsupported ||
!widget._managerAPI
.isVersionCompatibilityCheckEnabled()) {
widget.onChanged(widget.isSelected);
}
},
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.only(top: 10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.simpleName,
maxLines: 2,
overflow: TextOverflow.visible,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
Text(
widget.description,
softWrap: true,
overflow: TextOverflow.visible,
style: TextStyle(
fontSize: 14,
color: Theme.of(context)
.colorScheme
.onSecondaryContainer,
),
),
if (widget.description.isNotEmpty)
Align(
alignment: Alignment.topLeft,
child: Wrap(
spacing: 8,
runSpacing: 4,
children: [
if (widget.isUnsupported &&
!widget._managerAPI
.isVersionCompatibilityCheckEnabled())
Padding(
padding: const EdgeInsets.only(top: 8),
child: TextButton.icon(
label: I18nText('warning'),
icon: const Icon(
Icons.warning_amber_outlined,
size: 20.0,
),
onPressed: () =>
_showUnsupportedWarningDialog(),
style: ButtonStyle(
shape: MaterialStateProperty.all(
RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(8),
side: BorderSide(
color: Theme.of(context)
.colorScheme
.secondary,
),
),
),
backgroundColor:
MaterialStateProperty.all(
Colors.transparent,
),
foregroundColor:
MaterialStateProperty.all(
Theme.of(context).colorScheme.secondary,
),
),
),
),
],
),
),
],
),
],
),
),
Row(
children: [
if (widget.isUnsupported &&
widget._managerAPI.areExperimentalPatchesEnabled())
Padding(
padding: const EdgeInsets.only(top: 8, right: 8),
child: TextButton.icon(
label: I18nText('warning'),
icon: const Icon(Icons.warning, size: 20.0),
onPressed: () => _showUnsupportedWarningDialog(),
style: ButtonStyle(
shape: MaterialStateProperty.all(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: BorderSide(
color: Theme.of(context).colorScheme.secondary,
),
),
),
backgroundColor: MaterialStateProperty.all(
Colors.transparent,
),
foregroundColor: MaterialStateProperty.all(
Theme.of(context).colorScheme.secondary,
),
),
),
),
if (widget.isNew)
Padding(
padding: const EdgeInsets.only(top: 8),
child: TextButton.icon(
label: I18nText('new'),
icon: const Icon(Icons.star, size: 20.0),
onPressed: () => _showNewPatchDialog(),
style: ButtonStyle(
shape: MaterialStateProperty.all(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: BorderSide(
color: Theme.of(context).colorScheme.secondary,
),
),
),
backgroundColor: MaterialStateProperty.all(
Colors.transparent,
),
foregroundColor: MaterialStateProperty.all(
Theme.of(context).colorScheme.secondary,
),
),
),
),
],
),
widget.child ?? const SizedBox(),
if (widget.options.isNotEmpty)
IconButton(
icon: const Icon(Icons.settings_outlined),
onPressed: () => widget.navigateToOptions(widget.options),
),
],
),
),
@ -214,7 +222,7 @@ class _PatchItemState extends State<PatchItem> {
translationParams: {
'packageVersion': widget.packageVersion,
'supportedVersions':
'\u2022 ${widget.supportedPackageVersions.reversed.join('\n\u2022 ')}',
' ${widget.supportedPackageVersions.reversed.join('\n ')}',
},
),
actions: <Widget>[
@ -227,14 +235,14 @@ class _PatchItemState extends State<PatchItem> {
);
}
Future<void> _showNewPatchDialog() {
Future<void> _showUnsupportedRequiredOptionDialog() {
return showDialog(
context: context,
builder: (context) => AlertDialog(
title: I18nText('patchItem.newPatch'),
title: I18nText('notice'),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
content: I18nText(
'patchItem.newPatchDialogText',
'patchItem.unsupportedRequiredOption',
),
actions: <Widget>[
CustomMaterialButton(

View File

@ -1,73 +1,393 @@
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:revanced_manager/models/patch.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
class OptionsTextField extends StatelessWidget {
const OptionsTextField({Key? key, required this.hint}) : super(key: key);
final String hint;
class BooleanPatchOption extends StatelessWidget {
const BooleanPatchOption({
super.key,
required this.patchOption,
required this.removeOption,
required this.onChanged,
});
final Option patchOption;
final void Function(Option option) removeOption;
final void Function(dynamic value, Option option) onChanged;
@override
Widget build(BuildContext context) {
final size = MediaQuery.sizeOf(context);
final sHeight = size.height;
final sWidth = size.width;
return Container(
margin: const EdgeInsets.only(top: 12, bottom: 6),
padding: EdgeInsets.zero,
child: TextField(
decoration: InputDecoration(
constraints: BoxConstraints(
maxHeight: sHeight * 0.05,
maxWidth: sWidth * 1,
final ValueNotifier patchOptionValue = ValueNotifier(patchOption.value);
return PatchOption(
widget: Align(
alignment: Alignment.bottomLeft,
child: ValueListenableBuilder(
valueListenable: patchOptionValue,
builder: (context, value, child) {
return Switch(
value: value ?? false,
onChanged: (bool value) {
patchOptionValue.value = value;
onChanged(value, patchOption);
},
);
},
),
),
patchOption: patchOption,
removeOption: (Option option) {
removeOption(option);
},
);
}
}
class IntAndStringPatchOption extends StatelessWidget {
const IntAndStringPatchOption({
super.key,
required this.patchOption,
required this.removeOption,
required this.onChanged,
});
final Option patchOption;
final void Function(Option option) removeOption;
final void Function(dynamic value, Option option) onChanged;
@override
Widget build(BuildContext context) {
final ValueNotifier patchOptionValue = ValueNotifier(patchOption.value);
return PatchOption(
widget: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextFieldForPatchOption(
value: patchOption.value,
optionType: patchOption.optionClassType,
onChanged: (value) {
patchOptionValue.value = value;
onChanged(value, patchOption);
},
),
border: const OutlineInputBorder(),
labelText: hint,
ValueListenableBuilder(
valueListenable: patchOptionValue,
builder: (context, value, child) {
if (patchOption.required && value == null) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 8),
I18nText(
'patchOptionsView.requiredOption',
child: Text(
'',
style: TextStyle(
color: Theme.of(context).colorScheme.error,
),
),
),
],
);
} else {
return const SizedBox();
}
},
),
],
),
patchOption: patchOption,
removeOption: (Option option) {
removeOption(option);
},
);
}
}
class IntStringLongListPatchOption extends StatelessWidget {
const IntStringLongListPatchOption({
super.key,
required this.patchOption,
required this.removeOption,
required this.onChanged,
});
final Option patchOption;
final void Function(Option option) removeOption;
final void Function(dynamic value, Option option) onChanged;
@override
Widget build(BuildContext context) {
final String type = patchOption.optionClassType;
final List<dynamic> values = patchOption.value ?? [];
final ValueNotifier patchOptionValue = ValueNotifier(values);
return PatchOption(
widget: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ValueListenableBuilder(
valueListenable: patchOptionValue,
builder: (context, value, child) {
return ListView.builder(
shrinkWrap: true,
itemCount: value.length,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (BuildContext context, int index) {
final e = values[index];
return TextFieldForPatchOption(
value: e.toString(),
optionType: type,
onChanged: (newValue) {
values[index] = type == 'StringListPatchOption'
? newValue
: type == 'IntListPatchOption'
? int.parse(newValue)
: num.parse(newValue);
onChanged(values, patchOption);
},
removeValue: (value) {
patchOptionValue.value = List.from(patchOptionValue.value)
..removeAt(index);
values.removeAt(index);
onChanged(values, patchOption);
},
);
},
);
},
),
const SizedBox(height: 4),
Align(
alignment: Alignment.centerLeft,
child: TextButton(
onPressed: () {
if (type == 'StringListPatchOption') {
patchOptionValue.value = List.from(patchOptionValue.value)
..add('');
values.add('');
} else {
patchOptionValue.value = List.from(patchOptionValue.value)
..add(0);
values.add(0);
}
onChanged(values, patchOption);
},
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.add, size: 20),
I18nText(
'add',
child: const Text(
'',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
),
],
),
),
),
],
),
patchOption: patchOption,
removeOption: (Option option) {
removeOption(option);
},
);
}
}
class UnsupportedPatchOption extends StatelessWidget {
const UnsupportedPatchOption({super.key, required this.patchOption});
final Option patchOption;
@override
Widget build(BuildContext context) {
return PatchOption(
widget: Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: I18nText(
'patchOptionsView.unsupportedOption',
child: const Text(
'',
style: TextStyle(
fontSize: 16,
),
),
),
),
),
patchOption: patchOption,
removeOption: (_) {},
);
}
}
class PatchOption extends StatelessWidget {
const PatchOption({
super.key,
required this.widget,
required this.patchOption,
required this.removeOption,
});
final Widget widget;
final Option patchOption;
final void Function(Option option) removeOption;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: CustomCard(
child: Row(
children: [
Expanded(
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
patchOption.title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 4),
Text(
patchOption.description,
style: TextStyle(
fontSize: 14,
color: Theme.of(context)
.colorScheme
.onSecondaryContainer,
),
),
],
),
),
if (!patchOption.required)
IconButton(
onPressed: () => removeOption(patchOption),
icon: const Icon(Icons.delete),
),
],
),
const SizedBox(height: 4),
widget,
],
),
),
],
),
),
);
}
}
class OptionsFilePicker extends StatelessWidget {
const OptionsFilePicker({Key? key, required this.optionName})
: super(key: key);
final String optionName;
class TextFieldForPatchOption extends StatefulWidget {
const TextFieldForPatchOption({
super.key,
required this.value,
this.removeValue,
required this.onChanged,
required this.optionType,
});
final String? value;
final String optionType;
final void Function(dynamic value)? removeValue;
final void Function(dynamic value) onChanged;
@override
State<TextFieldForPatchOption> createState() =>
_TextFieldForPatchOptionState();
}
class _TextFieldForPatchOptionState extends State<TextFieldForPatchOption> {
final TextEditingController controller = TextEditingController();
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
I18nText(
optionName,
child: Text(
'',
style: GoogleFonts.inter(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
final bool isStringOption = widget.optionType.contains('String');
final bool isListOption = widget.optionType.contains('List');
controller.text = widget.value ?? '';
return TextFormField(
inputFormatters: [
if (widget.optionType.contains('Int'))
FilteringTextInputFormatter.allow(RegExp(r'[0-9]')),
if (widget.optionType.contains('Long'))
FilteringTextInputFormatter.allow(RegExp(r'^[0-9]*\.?[0-9]*')),
],
controller: controller,
keyboardType: isStringOption ? TextInputType.text : TextInputType.number,
decoration: InputDecoration(
suffixIcon: PopupMenuButton(
tooltip: FlutterI18n.translate(
context,
'patchOptionsView.tooltip',
),
ElevatedButton(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(
Theme.of(context).colorScheme.primary,
),
),
onPressed: () {
// pick files
},
child: Text(
'Select File',
style: TextStyle(
color: Theme.of(context).textTheme.bodyLarge?.color,
),
),
),
],
itemBuilder: (BuildContext context) {
return [
if (isListOption)
PopupMenuItem(
value: 'remove',
child: I18nText('remove'),
),
if (isStringOption && !isListOption) ...[
PopupMenuItem(
value: 'patchOptionsView.selectFilePath',
child: I18nText('patchOptionsView.selectFilePath'),
),
PopupMenuItem(
value: 'patchOptionsView.selectFolder',
child: I18nText('patchOptionsView.selectFolder'),
),
],
];
},
onSelected: (String selection) async {
switch (selection) {
case 'patchOptionsView.selectFilePath':
final result = await FilePicker.platform.pickFiles();
if (result != null && result.files.single.path != null) {
controller.text = result.files.single.path.toString();
widget.onChanged(controller.text);
}
break;
case 'patchOptionsView.selectFolder':
final result = await FilePicker.platform.getDirectoryPath();
if (result != null) {
controller.text = result;
widget.onChanged(controller.text);
}
break;
case 'remove':
widget.removeValue!(widget.value);
break;
}
},
),
hintStyle: TextStyle(
fontSize: 14,
color: Theme.of(context).colorScheme.onSecondaryContainer,
),
),
onChanged: (String value) {
widget.onChanged(value);
},
);
}
}

View File

@ -1,18 +1,14 @@
// ignore_for_file: prefer_const_constructors
import 'package:flutter/material.dart';
import 'package:flutter_i18n/widgets/I18nText.dart';
import 'package:revanced_manager/ui/views/settings/settingsFragment/settings_manage_api_url.dart';
import 'package:revanced_manager/ui/views/settings/settingsFragment/settings_manage_sources.dart';
import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_auto_update_patches.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_enable_patches_selection.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_experimental_patches.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_experimental_universal_patches.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_section.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_universal_patches.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_version_compatibility_check.dart';
final _settingsViewModel = SettingsViewModel();
class SAdvancedSection extends StatelessWidget {
const SAdvancedSection({super.key});
@ -21,85 +17,14 @@ class SAdvancedSection extends StatelessWidget {
Widget build(BuildContext context) {
return SettingsSection(
title: 'settingsView.advancedSectionTitle',
children: <Widget>[
SManageApiUrlUI(),
SManageSourcesUI(),
// SManageKeystorePasswordUI(),
children: const <Widget>[
SAutoUpdatePatches(),
SEnablePatchesSelection(),
SExperimentalUniversalPatches(),
SExperimentalPatches(),
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText(
'settingsView.regenerateKeystoreLabel',
child: const Text(
'',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
),
),
),
subtitle: I18nText('settingsView.regenerateKeystoreHint'),
onTap: () => _showDeleteKeystoreDialog(context),
),
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText(
'settingsView.deleteTempDirLabel',
child: const Text(
'',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
),
),
),
subtitle: I18nText('settingsView.deleteTempDirHint'),
onTap: () => _settingsViewModel.deleteTempDir(),
),
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText(
'settingsView.deleteLogsLabel',
child: const Text(
'',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
),
),
),
subtitle: I18nText('settingsView.deleteLogsHint'),
onTap: () => _settingsViewModel.deleteLogs(),
),
SVersionCompatibilityCheck(),
SUniversalPatches(),
SManageSourcesUI(),
SManageApiUrlUI(),
],
);
}
Future<void> _showDeleteKeystoreDialog(context) {
return showDialog(
context: context,
builder: (context) => AlertDialog(
title: I18nText('settingsView.regenerateKeystoreDialogTitle'),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
content: I18nText('settingsView.regenerateKeystoreDialogText'),
actions: <Widget>[
CustomMaterialButton(
isFilled: false,
label: I18nText('noButton'),
onPressed: () => Navigator.of(context).pop(),
),
CustomMaterialButton(
label: I18nText('yesButton'),
onPressed: () => {
Navigator.of(context).pop(),
_settingsViewModel.deleteKeystore(),
},
),
],
),
);
}
}

View File

@ -0,0 +1,68 @@
import 'package:flutter/material.dart';
import 'package:flutter_i18n/widgets/I18nText.dart';
import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/settingsView/about_widget.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_section.dart';
final _settingsViewModel = SettingsViewModel();
class SDebugSection extends StatelessWidget {
const SDebugSection({super.key});
@override
Widget build(BuildContext context) {
return SettingsSection(
title: 'settingsView.debugSectionTitle',
children: <Widget>[
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText(
'settingsView.logsLabel',
child: const Text(
'',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
),
),
),
subtitle: I18nText('settingsView.logsHint'),
onTap: () => _settingsViewModel.exportLogcatLogs(),
),
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText(
'settingsView.deleteLogsLabel',
child: const Text(
'',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
),
),
),
subtitle: I18nText('settingsView.deleteLogsHint'),
onTap: () => _settingsViewModel.deleteLogs(),
),
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText(
'settingsView.deleteTempDirLabel',
child: const Text(
'',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
),
),
),
subtitle: I18nText('settingsView.deleteTempDirHint'),
onTap: () => _settingsViewModel.deleteTempDir(),
),
const AboutWidget(
padding: EdgeInsets.symmetric(horizontal: 20.0),
),
],
);
}
}

View File

@ -45,6 +45,46 @@ class SExportSection extends StatelessWidget {
subtitle: I18nText('settingsView.importPatchesHint'),
onTap: () => _settingsViewModel.importPatches(context),
),
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText(
'settingsView.resetStoredPatchesLabel',
child: const Text(
'',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
),
),
),
subtitle: I18nText('settingsView.resetStoredPatchesHint'),
onTap: () => _showResetDialog(
context,
'settingsView.resetStoredPatchesDialogTitle',
'settingsView.resetStoredPatchesDialogText',
_settingsViewModel.resetSelectedPatches,
),
),
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText(
'settingsView.resetStoredOptionsLabel',
child: const Text(
'',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
),
),
),
subtitle: I18nText('settingsView.resetStoredOptionsHint'),
onTap: () => _showResetDialog(
context,
'settingsView.resetStoredOptionsDialogTitle',
'settingsView.resetStoredOptionsDialogText',
_settingsViewModel.resetAllOptions,
),
),
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText(
@ -84,7 +124,7 @@ class SExportSection extends StatelessWidget {
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText(
'settingsView.resetStoredPatchesLabel',
'settingsView.regenerateKeystoreLabel',
child: const Text(
'',
style: TextStyle(
@ -93,22 +133,26 @@ class SExportSection extends StatelessWidget {
),
),
),
subtitle: I18nText('settingsView.resetStoredPatchesHint'),
onTap: () => _showResetStoredPatchesDialog(context),
subtitle: I18nText('settingsView.regenerateKeystoreHint'),
onTap: () => _showDeleteKeystoreDialog(context),
),
// SManageKeystorePasswordUI(),
],
);
}
Future<void> _showResetStoredPatchesDialog(context) {
Future<void> _showResetDialog(
context,
dialogTitle,
dialogText,
dialogAction,
) {
return showDialog(
context: context,
builder: (context) => AlertDialog(
title: I18nText('settingsView.resetStoredPatchesDialogTitle'),
title: I18nText(dialogTitle),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
content: I18nText(
'settingsView.resetStoredPatchesDialogText',
),
content: I18nText(dialogText),
actions: <Widget>[
CustomMaterialButton(
isFilled: false,
@ -119,7 +163,32 @@ class SExportSection extends StatelessWidget {
label: I18nText('yesButton'),
onPressed: () => {
Navigator.of(context).pop(),
_settingsViewModel.resetSelectedPatches(),
dialogAction(),
},
),
],
),
);
}
Future<void> _showDeleteKeystoreDialog(context) {
return showDialog(
context: context,
builder: (context) => AlertDialog(
title: I18nText('settingsView.regenerateKeystoreDialogTitle'),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
content: I18nText('settingsView.regenerateKeystoreDialogText'),
actions: <Widget>[
CustomMaterialButton(
isFilled: false,
label: I18nText('noButton'),
onPressed: () => Navigator.of(context).pop(),
),
CustomMaterialButton(
label: I18nText('yesButton'),
onPressed: () => {
Navigator.of(context).pop(),
_settingsViewModel.deleteKeystore(),
},
),
],

View File

@ -1,38 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_i18n/widgets/I18nText.dart';
import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/settingsView/about_widget.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_section.dart';
final _settingsViewModel = SettingsViewModel();
class SInfoSection extends StatelessWidget {
const SInfoSection({super.key});
@override
Widget build(BuildContext context) {
return SettingsSection(
title: 'settingsView.infoSectionTitle',
children: <Widget>[
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText(
'settingsView.logsLabel',
child: const Text(
'',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
),
),
),
subtitle: I18nText('settingsView.logsHint'),
onTap: () => _settingsViewModel.exportLogcatLogs(),
),
const AboutWidget(
padding: EdgeInsets.symmetric(horizontal: 20.0),
),
],
);
}
}

View File

@ -4,26 +4,26 @@ 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/settings_viewmodel.dart';
class SExperimentalUniversalPatches extends StatefulWidget {
const SExperimentalUniversalPatches({super.key});
class SUniversalPatches extends StatefulWidget {
const SUniversalPatches({super.key});
@override
State<SExperimentalUniversalPatches> createState() =>
_SExperimentalUniversalPatchesState();
State<SUniversalPatches> createState() =>
_SUniversalPatchesState();
}
final _settingsViewModel = SettingsViewModel();
final _patchesSelectorViewModel = PatchesSelectorViewModel();
final _patcherViewModel = PatcherViewModel();
class _SExperimentalUniversalPatchesState
extends State<SExperimentalUniversalPatches> {
class _SUniversalPatchesState
extends State<SUniversalPatches> {
@override
Widget build(BuildContext context) {
return SwitchListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText(
'settingsView.experimentalUniversalPatchesLabel',
'settingsView.universalPatchesLabel',
child: const Text(
'',
style: TextStyle(
@ -32,7 +32,7 @@ class _SExperimentalUniversalPatchesState
),
),
),
subtitle: I18nText('settingsView.experimentalUniversalPatchesHint'),
subtitle: I18nText('settingsView.universalPatchesHint'),
value: _settingsViewModel.areUniversalPatchesEnabled(),
onChanged: (value) {
setState(() {

View File

@ -5,24 +5,24 @@ import 'package:revanced_manager/ui/views/patches_selector/patches_selector_view
import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart';
import 'package:revanced_manager/utils/check_for_supported_patch.dart';
class SExperimentalPatches extends StatefulWidget {
const SExperimentalPatches({super.key});
class SVersionCompatibilityCheck extends StatefulWidget {
const SVersionCompatibilityCheck({super.key});
@override
State<SExperimentalPatches> createState() => _SExperimentalPatchesState();
State<SVersionCompatibilityCheck> createState() => _SVersionCompatibilityCheckState();
}
final _settingsViewModel = SettingsViewModel();
final _patchesSelectorViewModel = PatchesSelectorViewModel();
final _patcherViewModel = PatcherViewModel();
class _SExperimentalPatchesState extends State<SExperimentalPatches> {
class _SVersionCompatibilityCheckState extends State<SVersionCompatibilityCheck> {
@override
Widget build(BuildContext context) {
return SwitchListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText(
'settingsView.experimentalPatchesLabel',
'settingsView.versionCompatibilityCheckLabel',
child: const Text(
'',
style: TextStyle(
@ -31,11 +31,11 @@ class _SExperimentalPatchesState extends State<SExperimentalPatches> {
),
),
),
subtitle: I18nText('settingsView.experimentalPatchesHint'),
value: _settingsViewModel.areExperimentalPatchesEnabled(),
subtitle: I18nText('settingsView.versionCompatibilityCheckHint'),
value: _settingsViewModel.isVersionCompatibilityCheckEnabled(),
onChanged: (value) {
setState(() {
_settingsViewModel.useExperimentalPatches(value);
_settingsViewModel.useVersionCompatibilityCheck(value);
});
if (!value) {
_patcherViewModel.selectedPatches

View File

@ -4,6 +4,7 @@ import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:revanced_manager/ui/widgets/settingsView/social_media_item.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_icon.dart';
class SocialMediaWidget extends StatelessWidget {
const SocialMediaWidget({
@ -42,6 +43,12 @@ class SocialMediaWidget extends StatelessWidget {
child: const CustomCard(
child: Column(
children: <Widget>[
SocialMediaItem(
icon: Icon(CustomIcon.revancedIcon),
title: Text('Website'),
subtitle: Text('revanced.app'),
url: 'https://revanced.app',
),
SocialMediaItem(
icon: FaIcon(FontAwesomeIcons.github),
title: Text('GitHub'),

View File

@ -0,0 +1,9 @@
import 'package:flutter/widgets.dart';
class CustomIcon {
CustomIcon._();
static const _kFontFam = 'CustomIcon';
static const IconData revancedIcon = IconData(0xe800, fontFamily: _kFontFam);
}

View File

@ -1,6 +1,7 @@
import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/models/patch.dart';
import 'package:revanced_manager/models/patched_application.dart';
import 'package:revanced_manager/services/manager_api.dart';
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
bool isPatchSupported(Patch patch) {
@ -12,3 +13,49 @@ bool isPatchSupported(Patch patch) {
(pack.versions.isEmpty || pack.versions.contains(app.version)),
);
}
bool hasUnsupportedRequiredOption(List<Option> options, Patch patch) {
final List<String> requiredOptionsType = [];
final List<String> supportedOptionsType = [
'StringPatchOption',
'BooleanPatchOption',
'IntPatchOption',
'StringListPatchOption',
'IntListPatchOption',
'LongListPatchOption',
];
for (final Option option in options) {
if (option.required &&
option.value == null &&
locator<ManagerAPI>()
.getPatchOption(
locator<PatcherViewModel>().selectedApp!.packageName,
patch.name,
option.key,
) == null) {
requiredOptionsType.add(option.optionClassType);
}
}
for (final String optionType in requiredOptionsType) {
if (!supportedOptionsType.contains(optionType)) {
return true;
}
}
return false;
}
List<Option> getNullRequiredOptions(List<Patch> patches, String packageName) {
final List<Option> requiredNullOptions = [];
for (final patch in patches) {
for (final patchOption in patch.options) {
if (!patch.excluded &&
patchOption.required &&
patchOption.value == null &&
locator<ManagerAPI>()
.getPatchOption(packageName, patch.name, patchOption.key) == null) {
requiredNullOptions.add(patchOption);
}
}
}
return requiredNullOptions;
}

View File

@ -4,7 +4,7 @@ homepage: https://github.com/revanced/revanced-manager
publish_to: 'none'
version: 1.11.0+101100000
version: 1.13.1+101300100
environment:
sdk: '>=3.0.0 <4.0.0'
@ -75,6 +75,7 @@ dependencies:
flutter_markdown: ^0.6.14
dio_cache_interceptor: ^3.4.0
install_plugin: ^2.1.0
screenshot_callback: ^3.0.1
synchronized: ^3.1.0
dev_dependencies:
@ -90,5 +91,9 @@ dev_dependencies:
flutter:
uses-material-design: true
fonts:
- family: CustomIcon
fonts:
- asset: fonts/custom-icons.ttf
assets:
- assets/i18n/

2
settings.gradle Normal file
View File

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