Compare commits

...

101 Commits

Author SHA1 Message Date
d9acd0d74b chore: Merge branch dev to main (#1329) 2023-10-05 01:35:08 +02:00
7ae09159ba build: Bump version to v1.11.0 2023-10-05 01:23:48 +02:00
a709abd80c perf: Do not load patches twice (#1328) 2023-10-04 21:58:25 +02:00
cd07f39b69 fix: reset patches after patching 2023-10-04 12:16:56 -07:00
f7c11d07a8 fix(export-settings): export patches as json object 2023-10-04 11:33:13 -07:00
b07439d402 fix: Reload patches 2023-10-04 19:38:34 +02:00
d8eadc2a2d feat: Use simpler wording 2023-10-04 19:01:17 +02:00
3a88d4d3e6 build: Bump dependencies 2023-10-04 18:39:24 +02:00
012110f008 perf: Do not load patches twice 2023-10-04 18:39:24 +02:00
4de274bf62 feat: Export settings migration activity (#1308) 2023-10-01 19:12:31 +02:00
76b89baee3 build: Bump dependencies (#1311) 2023-10-01 19:03:26 +02:00
697ae92031 Apply suggestions from code review [skip ci] 2023-10-01 19:02:49 +02:00
c87f92b346 feat: Adjust install dialog labels 2023-10-01 06:21:03 +02:00
6961bb7fd0 fix: Do not delete cached downloads 2023-10-01 04:48:24 +02:00
6e26130744 chore: Add todo 2023-09-30 22:15:46 +02:00
123a375a27 refactor: Remove unused strings 2023-09-30 22:14:51 +02:00
2b4b3ca0a5 fix: Retrieve app information from patched app 2023-09-30 21:40:03 +02:00
c4a795418f fix: Move installation log to correct place 2023-09-30 21:13:32 +02:00
91837ebade feat: Remove original package name in app info view 2023-09-30 21:07:26 +02:00
0492e910ea fix: Fill the preferred action 2023-09-30 20:11:53 +02:00
36c86e22b1 fix: Load installed apps 2023-09-30 20:03:09 +02:00
6bdc0c7bb2 feat: Simplify label 2023-09-30 19:58:45 +02:00
1e8d8f749a fix: do not ask for patches consent before initializing model 2023-09-30 14:57:48 +05:45
2e8e3b0d1e fix: Do not hardcode any predefined packages 2023-09-29 20:12:39 +02:00
15b8613d3c feat: Only log relevant records 2023-09-29 19:40:22 +02:00
8ce266bc94 perf: Reduce amount of network requests 2023-09-29 18:39:07 +02:00
8661d72e45 feat: Simplify label 2023-09-29 17:00:34 +02:00
62505f2543 build: Bump dependencies 2023-09-28 17:36:10 +02:00
37986c58ec docs(building): correct path to gradle.properties 2023-09-28 21:45:41 +07:00
2968d96fe9 remove log import 2023-09-27 14:42:11 -07:00
e7c8d0e78c use same fingerprint 2023-09-27 14:36:39 -07:00
83cbb34a5b use revanced fingerprint 2023-09-27 14:27:38 -07:00
7559c7b67e verify fingerprint of calling app 2023-09-27 14:17:29 -07:00
02822f4b38 remove user interaction 2023-09-27 13:47:59 -07:00
96736afb94 make bars transparent 2023-09-27 12:32:19 -07:00
72ae132fcd make export settings activity transparent 2023-09-27 12:21:27 -07:00
2250e1bcab convert Booleans to Ints 2023-09-26 16:46:49 -07:00
d9d5b746c3 Added ExportSettingsActivity 2023-09-26 16:21:46 -07:00
f1ea306291 change booleans to numbers 2023-09-25 14:42:40 -07:00
378d62395a remove newlines from base64 output 2023-09-25 10:15:56 -07:00
99c92069b9 export saved patches and keystore 2023-09-25 08:43:10 -07:00
2a89ef797f feat: share settings 2023-09-24 16:56:25 -07:00
5838550188 build: bump version to v1.10.3 2023-09-23 13:10:46 +03:00
e0e01ae3ee chore: merge dev to main (#1300) 2023-09-23 13:10:08 +03:00
0983ba8a0f fix: search bar overflow (#1301) 2023-09-23 00:51:25 +03:00
0bfa776ce7 fix: npe when loading patch bundle on android 8 2023-09-23 00:24:17 +03:00
d2b09936d1 chore: merge dev to main (#1295) 2023-09-22 22:08:13 +07:00
68e9f0f7c1 build: bump version 1.10.2
!!
2023-09-22 22:05:31 +07:00
c3d345de80 fix: force disable material you on Android 11 and below (#1293) 2023-09-22 21:05:13 +07:00
385c0e246a build: use correct version code
The user won't notice it :shhh: we're fine, continue as normal.
2023-09-21 19:32:10 +07:00
5ead49a5b7 build: bump version to v1.10.1 2023-09-20 20:06:19 -07:00
c0760b1347 chore: merge dev to main (#1289) 2023-09-20 20:04:10 -07:00
e01b323aee fix: make entire theme item clickable 2023-09-20 20:03:47 -07:00
6f4866ef63 fix: default theme not following system (#1288) 2023-09-20 20:03:15 -07:00
1b6d72661c build: bump version to v1.10.0 2023-09-20 17:40:23 -07:00
c59d4aea81 chore: merge dev to main (#1239) 2023-09-20 17:35:01 -07:00
6260a80738 feat(settings - appearance): add system option (#1279)
Closes #1260
2023-09-21 03:25:23 +03:00
e75d3c8273 fix(install-type): update padding and enable radio list scrolling (#1287)
fix(install-type): update padding and enable radio list scrolling
2023-09-20 17:24:37 -07:00
b7acb475e9 fix: update install type dialog padding 2023-09-20 16:57:13 -07:00
42b6bbff7c fix: update youtube link (#1286) 2023-09-21 02:16:55 +03:00
4b8542b35b fix: load patches via PatchBundle (#1242) 2023-09-21 01:35:32 +03:00
9ad1d6cbfb fix(custom-sources): ignore casing when checking if default repo is being used (#1281) 2023-09-21 00:42:29 +03:00
4cdd9acd73 docs(readme): add documentation and minor fixes (#1264)
Co-authored-by: Pun Butrach <pun.butrach@gmail.com>
2023-09-16 17:38:06 +07:00
f4b0a695d6 fix: improve app list loading speed (#1166) 2023-09-15 22:58:15 +07:00
b525ea1ba4 ci: remove analyze workflow (#1262)
Code style is not enforced so analysis is not needed.
2023-09-12 23:40:00 +07:00
c1fc2c4766 ci: bump actions/checkout to v4 2023-09-10 14:43:58 +07:00
5c733932c7 ci(pr-build): revert "sign apk with keystore (#1231)"
This reverts commit 8bf08ff4641451db64c80fddc9a3281cdf73098b, as it fails for PRs originating from forks
2023-09-08 01:14:13 +02:00
d1218616ec docs(readme): minor improvements 2023-09-08 01:14:13 +02:00
2bf6a03d56 fix: back button closing the app from any page 2023-09-08 01:14:13 +02:00
b6ee63c1ea ci(pr-build): sign apk with keystore (#1231) 2023-09-08 01:14:13 +02:00
6d08efdcd7 chore: fix issue template 2023-09-08 01:13:42 +02:00
a0a43a5651 build: bump version to v1.9.5 2023-09-08 01:13:42 +02:00
3af2f5b032 chore: merge dev to main (#1177) 2023-09-04 04:15:12 +03:00
8f54b226b4 refactor(patches-selector): improve universal patches header 2023-09-03 21:14:09 +03:00
9f64011b26 fix: npe when patching on android 8 2023-09-03 20:54:42 +03:00
c5fc54e721 fix(installer): open the patched app after install (#1233) 2023-09-03 21:48:28 +07:00
fc8a4fc5b6 fix: Don't use 'BuildContext's across async gaps. (#1148) 2023-09-03 01:47:20 +03:00
6f9ab232ae chore: ignore the root .gradle folder (#1160) 2023-09-02 09:29:49 -07:00
8cb96f1e45 fix: permissions handling at first launch 2023-08-31 19:34:12 +05:45
5733acb77a build(dependency): update patcher to v14.2.2
https://github.com/ReVanced/revanced-patcher/releases/tag/v14.2.2
2023-08-31 20:35:24 +07:00
e49bcb2a69 chore: simplify issue templates (#1165) 2023-08-31 17:36:29 +07:00
42e41c399f fix: hide install button on error 2023-08-28 19:05:30 +03:00
166a3180d3 build: correct version code 2023-08-28 03:20:15 +03:00
3bf4982f23 chore: merge dev to main (#1163) 2023-08-28 02:47:47 +03:00
f4e1cccfac build: bump version to v1.9.4 2023-08-28 01:44:46 +02:00
7911a8f49e fix: properly log messages and progress 2023-08-28 01:44:46 +02:00
64a96fc3ce fix: close before returning 2023-08-28 01:44:46 +02:00
8e2cfbddc5 fix: ignore the root .gradle folder 2023-08-27 14:05:39 -07:00
45fae3f0fd build: bump ReVanced Patcher back to v14.2.1
This reverts the previous regression with the dependency to ReVanced Patcher.
2023-08-27 22:35:03 +02:00
e45a7824c1 build: bump version to v1.9.3 2023-08-27 13:27:53 +07:00
5d72c48a76 chore: merge dev to main (#1157) 2023-08-27 13:27:01 +07:00
d6169c6fa2 fix: broken settings page 2023-08-27 11:55:21 +05:45
9df6d52e2d build: bump version to v1.9.2 2023-08-27 05:40:50 +03:00
239de8e923 chore: merge dev to main (#1156) 2023-08-27 05:40:13 +03:00
7d553a87f3 build: revert patcher to v11.0.4 2023-08-27 05:39:24 +03:00
557b42bc56 build: bump version to 1.9.1 2023-08-27 03:58:30 +03:00
8423914748 chore: merge dev to main (#1155) 2023-08-27 03:57:57 +03:00
07dce23794 build: bump patcher to v14.2.0 2023-08-27 03:55:56 +03:00
18fd0552db build: bump version to v1.9.0 2023-08-27 02:23:36 +03:00
d537d48f8e chore: merge dev to main (#1125) 2023-08-27 02:21:48 +03:00
b456512bbb build: bump patcher to v14.1.0 (#1153)
Co-authored-by: aAbed <aabedhkhan@gmail.com>
2023-08-27 02:21:16 +03:00
39 changed files with 881 additions and 1117 deletions

View File

@ -1,5 +1,5 @@
name: 🐞 Bug report
description: Report a very clearly broken issue.
description: Create a new bug report.
title: 'bug: <title>'
labels: [bug]
body:
@ -8,53 +8,20 @@ body:
value: |
# ReVanced Manager bug report
Important to note that your issue may have already been reported before. Please check for existing issues [here](https://github.com/revanced/revanced-manager/labels/bug).
- type: dropdown
attributes:
label: Type
options:
- Error while running the manager
- Error at runtime
- Cosmetic
- Other
validations:
required: true
Please check for existing issues [here](https://github.com/revanced/revanced-manager/labels/bug) before creating a new one.
- type: textarea
attributes:
label: Bug description
description: How did you find the bug? Any additional details that might help?
description: |
- Describe your bug in detail
- Add steps to reproduce the bug if possible (Step 1. Download some files. Step 2. ...)
- Add images and videos if possible
- List selected patches if applicable
validations:
required: true
- type: textarea
attributes:
label: Steps to reproduce
description: Add the steps to reproduce this bug, including your environment.
placeholder: Step 1. Download some files. Step 2. ...
validations:
required: true
- type: textarea
attributes:
label: Android version
description: Android version used.
validations:
required: true
- type: textarea
attributes:
label: Manager version
description: Manager version used.
validations:
required: true
- type: textarea
attributes:
label: Target package name
description: App you tried to patch.
validations:
required: true
- type: textarea
attributes:
label: Target package version.
description: Version of the app you tried to patch.
label: Version of ReVanced Manager and version & name of application you tried to patch
validations:
required: true
- type: dropdown
@ -64,57 +31,31 @@ body:
- Non-root
- Root
validations:
required: true
required: false
- type: textarea
attributes:
label: Patches selected.
description: Patches you selected for the app.
validations:
required: true
- type: textarea
attributes:
label: Device logs (exported using Manager settings).
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so there is no need for backticks.
label: Device logs
description: Export logs in ReVanced Manager settings.
render: shell
validations:
required: true
- type: textarea
attributes:
label: Installer logs (exported using Installer menu option) [unneeded if the issue is not during patching].
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so there is no need for backticks.
label: Patcher logs
description: Export logs in "Patcher" screen.
render: shell
validations:
required: false
- type: textarea
attributes:
label: Screenshots or video
description: Add screenshots or videos that show the bug here.
placeholder: Drag and drop the screenshots/videos into this box.
validations:
required: false
- type: textarea
attributes:
label: Solution
description: If applicable, add a possible solution.
validations:
required: false
- type: textarea
attributes:
label: Additional context
description: Add additional context here.
validations:
required: false
- type: checkboxes
id: acknowledgments
attributes:
label: Acknowledgments
description: Your issue will be closed if you haven't done these steps.
label: Acknowledgements
description: Your issue will be closed if you don't follow the checklist below!
options:
- label: I have searched the existing issues; this is new and no duplicate or related to another open issue.
- label: This request is not a duplicate of an existing issue.
required: true
- label: I have written a short but informative title.
- label: I have chosen an appropriate title.
required: true
- label: I properly filled out all of the requested information in this issue.
- label: All requested information has been provided properly.
required: true
- label: The issue is solely related to ReVanced Manager and not caused by patches.
- label: The issue is solely related to the ReVanced Manager
required: true

View File

@ -1,52 +1,42 @@
name: ⭐ Feature request
description: Create a detailed feature request.
description: Create a new feature request.
title: 'feat: <title>'
labels: [feature-request]
body:
- type: dropdown
- type: markdown
attributes:
label: Type
options:
- Functionality
- Cosmetic
- Other
validations:
required: true
value: |
# ReVanced Manager feature request
Please check for existing feature requests [here](https://github.com/revanced/revanced-manager/labels/bug) before creating a new one.
- type: textarea
attributes:
label: Issue
description: What is the current problem. Why does it require a feature request?
validations:
required: true
- type: textarea
attributes:
label: Feature
description: Describe your feature in detail. How does it solve the issue?
label: Feature description
description: Describe your feature in detail.
validations:
required: true
- type: textarea
attributes:
label: Motivation
description: Why should your feature should be considered?
description: Explain why the lack of it is a problem.
validations:
required: true
- type: textarea
attributes:
label: Additional context
description: Add additional context here.
description: In case there is something else you want to add.
validations:
required: false
- type: checkboxes
id: acknowledgements
attributes:
label: Acknowledgements
description: Your issue will be closed if you haven't done these steps.
description: Your issue will be closed if you don't follow the checklist below!
options:
- label: I have searched the existing issues and this is a new and no duplicate or related to another open issue.
- label: This request is not a duplicate of an existing issue.
required: true
- label: I have written a short but informative title.
- label: I have chosen an appropriate title.
required: true
- label: I filled out all of the requested information in this issue properly.
- label: All requested information has been provided properly.
required: true
- label: The issue is related solely to the ReVanced Manager
- label: The issue is solely related to the ReVanced Manager
required: true

View File

@ -1,38 +0,0 @@
name: Analyze Code
on:
push:
branches: [ "dev" ]
paths:
- "**.dart"
- ".github/workflows/analyze.yml"
pull_request:
branches: [ "main", "dev" ]
types:
- opened
- reopened
- synchronize
- ready_for_review
paths:
- "**.dart"
- ".github/workflows/analyze.yml"
jobs:
build:
name: "Static analysis & format check"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
channel: 'stable'
cache: true
- name: Install Flutter dependencies
run: flutter pub get
- name: Generate files with Builder
run: flutter packages pub run build_runner build --delete-conflicting-outputs
- name: Analyze code
uses: ValentinVignal/action-dart-analyze@v0.15
with:
fail-on: warning

View File

@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
# Make sure the release step uses its own credentials:
# https://github.com/cycjimmy/semantic-release-action#private-packages

View File

@ -9,7 +9,7 @@ jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set env
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
- name: Set up JDK 11

1
.gitignore vendored
View File

@ -58,6 +58,7 @@ unlinked.ds
unlinked_spec.ds
# Android related
.gradle/
**/android/**/gradle-wrapper.jar
**/android/.gradle
**/android/captures/

View File

@ -3,29 +3,33 @@
The official ReVanced Manager based on Flutter.
## 🔽 Download
To download latest Manager, go [here](https://github.com/revanced/revanced-manager/releases/latest) and install the provided APK file.
You can obtain ReVanced Manager by downloading it from either [revanced.app/download](https://revanced.app/download) or [GitHub Releases](https://github.com/ReVanced/revanced-manager/releases)
## 📝 Prerequisites
1. Android 8 or higher
2. Does not work on some armv7 devices
2. Incompatible with certain ARMv7 devices
## 📃 Documentation
The documentation can be found [here](https://github.com/revanced/revanced-manager/tree/main/docs).
## 🔴 Issues
For suggestions and bug reports, open an issue [here](https://github.com/revanced/revanced-manager/issues/new/choose).
## 💭 Discussion
If you wish to discuss the Manager, a thread has been made under the [#development](https://discord.com/channels/952946952348270622/1002922226443632761) channel in the Discord server, please note that this thread may be temporary and may be removed in the future.
## 🌐 Translation
[![Crowdin](https://badges.crowdin.net/revanced/localized.svg)](https://crowdin.com/project/revanced)
If you wish to translate ReVanced Manager, we're accepting translations on [Crowdin](https://translate.revanced.app)
We're accepting translations on [Crowdin](https://translate.revanced.app).
## 🛠️ Building Manager from source
1. Setup flutter environment for your [platform](https://docs.flutter.dev/get-started/install)
2. Clone the repository locally
3. Add your github token in gradle.properties like [this](/docs/4_building.md)
3. Add your GitHub token in gradle.properties like [this](/docs/4_building.md)
4. Open the project in terminal
5. Run `flutter pub get` in terminal
6. Then `flutter packages pub run build_runner build --delete-conflicting-outputs` (Must be done on each git pull)
7. To build release apk run `flutter build apk`
7. To build release APK run `flutter build apk`

View File

@ -52,6 +52,8 @@ android {
buildTypes {
release {
shrinkResources false
minifyEnabled false
resValue "string", "app_name", "ReVanced Manager"
signingConfig signingConfigs.debug
ndk {
@ -83,10 +85,9 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
// ReVanced
implementation "app.revanced:revanced-patcher:11.0.4"
implementation "app.revanced:revanced-patcher:16.0.0"
// Signing & aligning
implementation("org.bouncycastle:bcpkix-jdk15on:1.70")
implementation("com.android.tools.build:apksig:7.2.2")
}

View File

@ -25,8 +25,7 @@
android:icon="@mipmap/ic_launcher"
android:largeHeap="true"
android:requestLegacyExternalStorage="true"
android:extractNativeLibs="true"
android:enableOnBackInvokedCallback="true">
android:extractNativeLibs="true">
<activity
android:name=".MainActivity"
android:exported="true"
@ -43,6 +42,10 @@
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity
android:name=".ExportSettingsActivity"
android:exported="true">
</activity>
<meta-data
android:name="flutterEmbedding"
android:value="2" />

View File

@ -0,0 +1,86 @@
package app.revanced.manager.flutter
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.os.Bundle
import android.util.Base64
import org.json.JSONObject
import java.io.ByteArrayInputStream
import java.io.File
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import java.security.MessageDigest
class ExportSettingsActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val callingPackageName = getCallingPackage()!!
if (getFingerprint(callingPackageName) == getFingerprint(getPackageName())) {
// Create JSON Object
val json = JSONObject()
// Default Data
json.put("keystorePassword", "s3cur3p@ssw0rd")
// Load Shared Preferences
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
)
}
// Load keystore
val keystoreFile = File(getExternalFilesDir(null), "/revanced-manager.keystore")
if (keystoreFile.exists()) {
val keystoreBytes = keystoreFile.readBytes()
val keystoreBase64 = Base64.encodeToString(keystoreBytes, Base64.DEFAULT)
json.put("keystore", keystoreBase64)
}
// Load saved patches
val storedPatchesFile = File(filesDir.parentFile.absolutePath, "/app_flutter/selected-patches.json")
if (storedPatchesFile.exists()) {
val patchesBytes = storedPatchesFile.readBytes()
val patches = String(patchesBytes, Charsets.UTF_8)
json.put("patches", JSONObject(patches))
}
// Send data back
val resultIntent = Intent()
resultIntent.putExtra("data", json.toString())
setResult(Activity.RESULT_OK, resultIntent)
finish()
} else {
val resultIntent = Intent()
setResult(Activity.RESULT_CANCELED)
finish()
}
}
fun getFingerprint(packageName: String): String {
// Get the signature of the app that matches the package name
val packageInfo = packageManager.getPackageInfo(packageName, PackageManager.GET_SIGNATURES)
val signature = packageInfo.signatures[0]
// Get the raw certificate data
val rawCert = signature.toByteArray()
// Generate an X509Certificate from the data
val certFactory = CertificateFactory.getInstance("X509")
val x509Cert = certFactory.generateCertificate(ByteArrayInputStream(rawCert)) as X509Certificate
// Get the SHA256 fingerprint
val fingerprint = MessageDigest.getInstance("SHA256").digest(x509Cert.encoded).joinToString("") {
"%02x".format(it)
}
return fingerprint
}
}

View File

@ -1,28 +1,30 @@
package app.revanced.manager.flutter
import android.os.Build
import android.os.Handler
import android.os.Looper
import androidx.annotation.NonNull
import app.revanced.manager.flutter.utils.Aapt
import app.revanced.manager.flutter.utils.aligning.ZipAligner
import app.revanced.manager.flutter.utils.signing.Signer
import app.revanced.manager.flutter.utils.zip.ZipFile
import app.revanced.manager.flutter.utils.zip.structures.ZipEntry
import app.revanced.patcher.PatchBundleLoader
import app.revanced.patcher.PatchSet
import app.revanced.patcher.Patcher
import app.revanced.patcher.PatcherOptions
import app.revanced.patcher.extensions.PatchExtensions.compatiblePackages
import app.revanced.patcher.extensions.PatchExtensions.patchName
import app.revanced.patcher.logging.Logger
import app.revanced.patcher.util.patch.PatchBundle
import dalvik.system.DexClassLoader
import app.revanced.patcher.patch.PatchResult
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import kotlinx.coroutines.cancel
import kotlinx.coroutines.runBlocking
import org.json.JSONArray
import org.json.JSONObject
import java.io.File
private const val PATCHER_CHANNEL = "app.revanced.manager.flutter/patcher"
private const val INSTALLER_CHANNEL = "app.revanced.manager.flutter/installer"
import java.io.PrintWriter
import java.io.StringWriter
import java.lang.Error
import java.util.logging.LogRecord
import java.util.logging.Logger
class MainActivity : FlutterActivity() {
private val handler = Handler(Looper.getMainLooper())
@ -30,14 +32,23 @@ class MainActivity : FlutterActivity() {
private var cancel: Boolean = false
private var stopResult: MethodChannel.Result? = null
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
private lateinit var patches: PatchSet
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
val mainChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, PATCHER_CHANNEL)
installerChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, INSTALLER_CHANNEL)
val patcherChannel = "app.revanced.manager.flutter/patcher"
val installerChannel = "app.revanced.manager.flutter/installer"
val mainChannel =
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, patcherChannel)
this.installerChannel =
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, installerChannel)
mainChannel.setMethodCallHandler { call, result ->
when (call.method) {
"runPatcher" -> {
val patchBundleFilePath = call.argument<String>("patchBundleFilePath")
val originalFilePath = call.argument<String>("originalFilePath")
val inputFilePath = call.argument<String>("inputFilePath")
val patchedFilePath = call.argument<String>("patchedFilePath")
@ -48,7 +59,7 @@ class MainActivity : FlutterActivity() {
val keyStoreFilePath = call.argument<String>("keyStoreFilePath")
val keystorePassword = call.argument<String>("keystorePassword")
if (patchBundleFilePath != null &&
if (
originalFilePath != null &&
inputFilePath != null &&
patchedFilePath != null &&
@ -62,7 +73,6 @@ class MainActivity : FlutterActivity() {
cancel = false
runPatcher(
result,
patchBundleFilePath,
originalFilePath,
inputFilePath,
patchedFilePath,
@ -73,14 +83,55 @@ class MainActivity : FlutterActivity() {
keyStoreFilePath,
keystorePassword
)
} else {
result.notImplemented()
}
} else result.notImplemented()
}
"stopPatcher" -> {
cancel = true
stopResult = result
}
"getPatches" -> {
val patchBundleFilePath = call.argument<String>("patchBundleFilePath")!!
val cacheDirPath = call.argument<String>("cacheDirPath")!!
try {
patches = PatchBundleLoader.Dex(
File(patchBundleFilePath),
optimizedDexDirectory = File(cacheDirPath)
)
} catch (ex: Exception) {
return@setMethodCallHandler result.notImplemented()
} catch (err: Error) {
return@setMethodCallHandler result.notImplemented()
}
JSONArray().apply {
patches.forEach {
JSONObject().apply {
put("name", it.name)
put("description", it.description)
put("excluded", !it.use)
put("compatiblePackages", JSONArray().apply {
it.compatiblePackages?.forEach { compatiblePackage ->
val compatiblePackageJson = JSONObject().apply {
put("name", compatiblePackage.name)
put(
"versions",
JSONArray().apply {
compatiblePackage.versions?.forEach { version ->
put(version)
}
})
}
put(compatiblePackageJson)
}
})
}.let(::put)
}
}.toString().let(result::success)
}
else -> result.notImplemented()
}
}
@ -88,7 +139,6 @@ class MainActivity : FlutterActivity() {
private fun runPatcher(
result: MethodChannel.Result,
patchBundleFilePath: String,
originalFilePath: String,
inputFilePath: String,
patchedFilePath: String,
@ -105,179 +155,141 @@ class MainActivity : FlutterActivity() {
val outFile = File(outFilePath)
val integrations = File(integrationsPath)
val keyStoreFile = File(keyStoreFilePath)
val cacheDir = File(cacheDirPath)
Thread {
try {
fun updateProgress(progress: Double, header: String, log: String) {
handler.post {
installerChannel.invokeMethod(
"update",
mapOf(
"progress" to 0.1,
"header" to "",
"log" to "Copying original apk"
"progress" to progress,
"header" to header,
"log" to log
)
)
}
}
if(cancel) {
handler.post { stopResult!!.success(null) }
fun postStop() = handler.post { stopResult!!.success(null) }
// Setup logger
Logger.getLogger("").apply {
handlers.forEach {
it.close()
removeHandler(it)
}
object : java.util.logging.Handler() {
override fun publish(record: LogRecord) {
if (record.loggerName?.startsWith("app.revanced") != true) return
updateProgress(-1.0, "", record.message)
}
override fun flush() = Unit
override fun close() = flush()
}.let(::addHandler)
}
try {
updateProgress(0.0, "", "Copying APK")
if (cancel) {
postStop()
return@Thread
}
originalFile.copyTo(inputFile, true)
handler.post {
installerChannel.invokeMethod(
"update",
mapOf(
"progress" to 0.2,
"header" to "Unpacking apk...",
"log" to "Unpacking input apk"
)
if (cancel) {
postStop()
return@Thread
}
updateProgress(0.05, "Reading APK...", "Reading APK")
val patcher = Patcher(
PatcherOptions(
inputFile,
cacheDir,
Aapt.binary(applicationContext).absolutePath,
cacheDir.path,
)
}
)
if(cancel) {
handler.post { stopResult!!.success(null) }
if (cancel) {
postStop()
return@Thread
}
val patcher =
Patcher(
PatcherOptions(
inputFile,
cacheDirPath,
Aapt.binary(applicationContext).absolutePath,
cacheDirPath,
logger = ManagerLogger()
)
)
updateProgress(0.1, "Loading patches...", "Loading patches")
if(cancel) {
handler.post { stopResult!!.success(null) }
val patches = patches.filter { patch ->
val isCompatible = patch.compatiblePackages?.any {
it.name == patcher.context.packageMetadata.packageName
} ?: false
val compatibleOrUniversal =
isCompatible || patch.compatiblePackages.isNullOrEmpty()
compatibleOrUniversal && selectedPatches.any { it == patch.name }
}
if (cancel) {
postStop()
return@Thread
}
handler.post {
installerChannel.invokeMethod(
"update",
mapOf("progress" to 0.3, "header" to "", "log" to "")
)
}
handler.post {
installerChannel.invokeMethod(
"update",
mapOf(
"progress" to 0.4,
"header" to "Merging integrations...",
"log" to "Merging integrations"
)
)
}
updateProgress(0.15, "Executing...", "")
if(cancel) {
handler.post { stopResult!!.success(null) }
return@Thread
}
// Update the progress bar every time a patch is executed from 0.15 to 0.7
val totalPatchesCount = patches.size
val progressStep = 0.55 / totalPatchesCount
var progress = 0.15
patcher.addIntegrations(listOf(integrations)) {}
patcher.apply {
acceptIntegrations(listOf(integrations))
acceptPatches(patches)
if(cancel) {
handler.post { stopResult!!.success(null) }
return@Thread
}
runBlocking {
apply(false).collect { patchResult: PatchResult ->
if (cancel) {
handler.post { stopResult!!.success(null) }
this.cancel()
this@apply.close()
return@collect
}
handler.post {
installerChannel.invokeMethod(
"update",
mapOf(
"progress" to 0.5,
"header" to "Applying patches...",
"log" to ""
)
)
}
val msg = patchResult.exception?.let {
val writer = StringWriter()
it.printStackTrace(PrintWriter(writer))
"${patchResult.patch.name} failed: $writer"
} ?: run {
"${patchResult.patch.name} succeeded"
}
if(cancel) {
handler.post { stopResult!!.success(null) }
return@Thread
}
val patches = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.CUPCAKE) {
PatchBundle.Dex(
patchBundleFilePath,
DexClassLoader(
patchBundleFilePath,
cacheDirPath,
null,
javaClass.classLoader
)
).loadPatches().filter { patch ->
(patch.compatiblePackages?.any { it.name == patcher.context.packageMetadata.packageName } == true || patch.compatiblePackages.isNullOrEmpty()) &&
selectedPatches.any { it == patch.patchName }
}
} else {
TODO("VERSION.SDK_INT < CUPCAKE")
}
if(cancel) {
handler.post { stopResult!!.success(null) }
return@Thread
}
patcher.addPatches(patches)
patcher.executePatches().forEach { (patch, res) ->
if (res.isSuccess) {
val msg = "Applied $patch"
handler.post {
installerChannel.invokeMethod(
"update",
mapOf(
"progress" to 0.5,
"header" to "",
"log" to msg
)
)
updateProgress(progress, "", msg)
progress += progressStep
}
if(cancel) {
handler.post { stopResult!!.success(null) }
return@Thread
}
return@forEach
}
val msg =
"Failed to apply $patch: " + "${res.exceptionOrNull()!!.message ?: res.exceptionOrNull()!!.cause!!::class.simpleName}"
handler.post {
installerChannel.invokeMethod(
"update",
mapOf("progress" to 0.5, "header" to "", "log" to msg)
)
}
if(cancel) {
handler.post { stopResult!!.success(null) }
return@Thread
}
}
handler.post {
installerChannel.invokeMethod(
"update",
mapOf(
"progress" to 0.7,
"header" to "Repacking apk...",
"log" to "Repacking patched apk"
)
)
}
if(cancel) {
handler.post { stopResult!!.success(null) }
if (cancel) {
postStop()
patcher.close()
return@Thread
}
val res = patcher.save()
updateProgress(0.8, "Building...", "")
val res = patcher.get()
patcher.close()
ZipFile(patchedFile).use { file ->
res.dexFiles.forEach {
if(cancel) {
handler.post { stopResult!!.success(null) }
if (cancel) {
postStop()
return@Thread
}
file.addEntryCompressData(
@ -296,90 +308,35 @@ class MainActivity : FlutterActivity() {
ZipAligner::getEntryAlignment
)
}
if(cancel) {
handler.post { stopResult!!.success(null) }
if (cancel) {
postStop()
return@Thread
}
handler.post {
installerChannel.invokeMethod(
"update",
mapOf(
"progress" to 0.9,
"header" to "Signing apk...",
"log" to ""
)
)
}
updateProgress(0.9, "Signing...", "Signing APK")
try {
Signer("ReVanced", keystorePassword).signApk(
patchedFile,
outFile,
keyStoreFile
)
Signer("ReVanced", keystorePassword)
.signApk(patchedFile, outFile, keyStoreFile)
} catch (e: Exception) {
//log to console
print("Error signing apk: ${e.message}")
print("Error signing APK: ${e.message}")
e.printStackTrace()
}
handler.post {
installerChannel.invokeMethod(
"update",
mapOf(
"progress" to 1.0,
"header" to "Finished!",
"log" to "Finished!"
)
)
}
updateProgress(1.0, "Patched", "Patched")
} catch (ex: Throwable) {
val stack = ex.stackTraceToString()
handler.post {
installerChannel.invokeMethod(
"update",
mapOf(
"progress" to -100.0,
"header" to "Aborted...",
"log" to "An error occurred! Aborted\nError:\n$stack"
)
if (!cancel) {
val stack = ex.stackTraceToString()
updateProgress(
-100.0,
"Failed",
"An error occurred:\n$stack"
)
}
}
handler.post { result.success(null) }
}.start()
}
inner class ManagerLogger : Logger {
override fun error(msg: String) {
handler.post {
installerChannel
.invokeMethod(
"update",
mapOf("progress" to -1.0, "header" to "", "log" to msg)
)
}
}
override fun warn(msg: String) {
handler.post {
installerChannel.invokeMethod(
"update",
mapOf("progress" to -1.0, "header" to "", "log" to msg)
)
}
}
override fun info(msg: String) {
handler.post {
installerChannel.invokeMethod(
"update",
mapOf("progress" to -1.0, "header" to "", "log" to msg)
)
}
}
override fun trace(_msg: String) { /* unused */
}
}
}

View File

@ -1,5 +1,5 @@
buildscript {
ext.kotlin_version = '1.7.10'
ext.kotlin_version = '1.9.0'
repositories {
google()
mavenCentral()

View File

@ -23,13 +23,13 @@
"widgetTitle": "Dashboard",
"updatesSubtitle": "Updates",
"patchedSubtitle": "Patched applications",
"patchedSubtitle": "Patched apps",
"noUpdates": "No updates available",
"WIP": "Work in progress...",
"noInstallations": "No patched applications installed",
"noInstallations": "No patched apps installed",
"installUpdate": "Continue to install the update?",
"updateDialogTitle": "Update Manager",
@ -56,9 +56,7 @@
"updatesDisabled": "Updating a patched app is currently disabled. Repatch the app again."
},
"applicationItem": {
"patchButton": "Patch",
"infoButton": "Info",
"changelogLabel": "Changelog"
"infoButton": "Info"
},
"latestCommitCard": {
"loadingLabel": "Loading...",
@ -71,9 +69,8 @@
"widgetTitle": "Patcher",
"patchButton": "Patch",
"patchDialogText": "You have selected a resource patch and a split APK installation has been detected, so patching errors may occur.\nAre you sure you want to proceed?",
"armv7WarningDialogText": "Patching on ARMv7 devices is not yet supported and might fail. Proceed anyways?",
"splitApkWarningDialogText": "Patching a split APK 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?"
},
"appSelectorCard": {
@ -148,9 +145,8 @@
"installTypeDescription": "Select the installation type to proceed with.",
"installButton": "Install",
"installRootType": "Root",
"installNonRootType": "Non-root",
"installRecommendedType": "Recommended",
"installRootType": "Mount",
"installNonRootType": "Normal",
"pressBackAgain": "Press back again to cancel",
"openButton": "Open",
@ -162,10 +158,6 @@
"exportApkButtonTooltip": "Export patched APK",
"exportLogButtonTooltip": "Export log",
"installErrorDialogTitle": "Error",
"installErrorDialogText1": "Root install is not possible with the current patches selection.\nRepatch your app or choose non-root install.",
"installErrorDialogText2": "Non-root install is not possible with the current patches selection.\nRepatch your app or choose root install if you have your device rooted.",
"installErrorDialogText3": "Root install is not possible as the original APK was selected from storage.\nSelect an installed app or choose non-root install.",
"noExit": "Installer is still running, cannot exit..."
},
"settingsView": {
@ -178,8 +170,10 @@
"exportSectionTitle": "Import & export",
"logsSectionTitle": "Logs",
"darkThemeLabel": "Dark mode",
"darkThemeHint": "Welcome to the dark side",
"themeModeLabel": "App theme",
"systemThemeLabel": "System",
"lightThemeLabel": "Light",
"darkThemeLabel": "Dark",
"dynamicThemeLabel": "Material You",
"dynamicThemeHint": "Enjoy an experience closer to your device",
@ -283,7 +277,6 @@
"rootDialogText": "App was installed with superuser permissions, but currently ReVanced Manager has no permissions.\nPlease grant superuser permissions first.",
"packageNameLabel": "Package name",
"originalPackageNameLabel": "Original package name",
"installTypeLabel": "Installation type",
"rootTypeLabel": "Root",
"nonRootTypeLabel": "Non-root",

View File

@ -12,7 +12,7 @@ This page will guide you through building ReVanced Manager from source.
3. Create a GitHub personal access token with the `read:packages` scope [here](https://github.com/settings/tokens/new?scopes=read:packages&description=ReVanced)
4. Add your GitHub username and the token to `~/.gradle/gradle.properties`
4. Add your GitHub username and the token to `~/android/gradle.properties`
```properties
gpr.user = YourUsername

View File

@ -1,5 +1,4 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:revanced_manager/utils/string.dart';
part 'patch.g.dart';
@ -9,26 +8,19 @@ class Patch {
required this.name,
required this.description,
required this.excluded,
required this.dependencies,
required this.compatiblePackages,
});
factory Patch.fromJson(Map<String, dynamic> json) => _$PatchFromJson(json);
final String name;
final String description;
final String? description;
final bool excluded;
final List<String> dependencies;
final List<Package> compatiblePackages;
Map<String, dynamic> toJson() => _$PatchToJson(this);
String getSimpleName() {
return name
.replaceAll('-', ' ')
.split('-')
.join(' ')
.toTitleCase()
.replaceFirst('Microg', 'MicroG');
return name;
}
}

View File

@ -9,23 +9,19 @@ class PatchedApplication {
PatchedApplication({
required this.name,
required this.packageName,
required this.originalPackageName,
required this.version,
required this.apkFilePath,
required this.icon,
required this.patchDate,
this.isRooted = false,
this.isFromStorage = false,
this.hasUpdates = false,
this.appliedPatches = const [],
this.changelog = const [],
});
factory PatchedApplication.fromJson(Map<String, dynamic> json) =>
_$PatchedApplicationFromJson(json);
String name;
String packageName;
String originalPackageName;
String version;
final String apkFilePath;
@JsonKey(
@ -36,9 +32,7 @@ class PatchedApplication {
DateTime patchDate;
bool isRooted;
bool isFromStorage;
bool hasUpdates;
List<String> appliedPatches;
List<String> changelog;
Map<String, dynamic> toJson() => _$PatchedApplicationToJson(this);

View File

@ -1,4 +1,3 @@
import 'dart:convert';
import 'dart:io';
import 'package:collection/collection.dart';
import 'package:dio/dio.dart';
@ -7,7 +6,6 @@ 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/models/patch.dart';
import 'package:revanced_manager/services/manager_api.dart';
@lazySingleton
@ -21,17 +19,6 @@ class GithubAPI {
priority: CachePriority.high,
);
final Map<String, String> repoAppPath = {
'com.google.android.youtube': 'youtube',
'com.google.android.apps.youtube.music': 'music',
'com.twitter.android': 'twitter',
'com.reddit.frontpage': 'reddit',
'com.zhiliaoapp.musically': 'tiktok',
'de.dwd.warnapp': 'warnwetter',
'com.garzotto.pflotsh.ecmwf_a': 'ecmwf',
'com.spotify.music': 'spotify',
};
Future<void> initialize(String repoUrl) async {
try {
_dio = Dio(
@ -142,38 +129,6 @@ class GithubAPI {
}
}
Future<List<String>> getCommits(
String packageName,
String repoName,
DateTime since,
) async {
final String path =
'src/main/kotlin/app/revanced/patches/${repoAppPath[packageName]}';
try {
final response = await _dio.get(
'/repos/$repoName/commits',
queryParameters: {
'path': path,
'since': since.toIso8601String(),
},
);
final List<dynamic> commits = response.data;
return commits
.map(
(commit) => commit['commit']['message'].split('\n')[0] +
' - ' +
commit['commit']['author']['name'] +
'\n' as String,
)
.toList();
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
}
return [];
}
Future<File?> getLatestReleaseFile(
String extension,
String repoName,
@ -222,10 +177,8 @@ class GithubAPI {
final String downloadUrl = asset['browser_download_url'];
if (extension == '.apk') {
_managerAPI.setIntegrationsDownloadURL(downloadUrl);
} else if (extension == '.json') {
_managerAPI.setPatchesDownloadURL(downloadUrl, false);
} else {
_managerAPI.setPatchesDownloadURL(downloadUrl, true);
_managerAPI.setPatchesDownloadURL(downloadUrl);
}
return await DefaultCacheManager().getSingleFile(
downloadUrl,
@ -239,30 +192,4 @@ class GithubAPI {
}
return null;
}
Future<List<Patch>> getPatches(
String repoName,
String version,
String url,
) async {
List<Patch> patches = [];
try {
final File? f = await getPatchesReleaseFile(
'.json',
repoName,
version,
url,
);
if (f != null) {
final List<dynamic> list = jsonDecode(f.readAsStringSync());
patches = list.map((patch) => Patch.fromJson(patch)).toList();
}
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
}
return patches;
}
}

View File

@ -11,6 +11,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/github_api.dart';
import 'package:revanced_manager/services/patcher_api.dart';
import 'package:revanced_manager/services/revanced_api.dart';
import 'package:revanced_manager/services/root_api.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
@ -26,6 +27,7 @@ class ManagerAPI {
final String patcherRepo = 'revanced-patcher';
final String cliRepo = 'revanced-cli';
late SharedPreferences _prefs;
List<Patch> patches = [];
bool isRooted = false;
String storedPatchesFile = '/selected-patches.json';
String keystoreFile =
@ -40,12 +42,14 @@ class ManagerAPI {
String defaultManagerRepo = 'revanced/revanced-manager';
String? patchesVersion = '';
String? integrationsVersion = '';
bool isDefaultPatchesRepo() {
return getPatchesRepo() == 'revanced/revanced-patches';
return getPatchesRepo().toLowerCase() == 'revanced/revanced-patches';
}
bool isDefaultIntegrationsRepo() {
return getIntegrationsRepo() == 'revanced/revanced-integrations';
return getIntegrationsRepo().toLowerCase() ==
'revanced/revanced-integrations';
}
Future<void> initialize() async {
@ -79,12 +83,12 @@ class ManagerAPI {
await _prefs.setString('repoUrl', url);
}
String getPatchesDownloadURL(bool bundle) {
return _prefs.getString('patchesDownloadURL-$bundle') ?? '';
String getPatchesDownloadURL() {
return _prefs.getString('patchesDownloadURL') ?? '';
}
Future<void> setPatchesDownloadURL(String value, bool bundle) async {
await _prefs.setString('patchesDownloadURL-$bundle', value);
Future<void> setPatchesDownloadURL(String value) async {
await _prefs.setString('patchesDownloadURL', value);
}
String getPatchesRepo() {
@ -111,17 +115,6 @@ class ManagerAPI {
}
bool isPatchesChangeEnabled() {
if (getPatchedApps().isNotEmpty && !isChangingToggleModified()) {
for (final apps in getPatchedApps()) {
if (getSavedPatches(apps.originalPackageName)
.indexWhere((patch) => patch.excluded) !=
-1) {
setPatchesChangeWarning(false);
setPatchesChangeEnabled(true);
break;
}
}
}
return _prefs.getBool('patchesChangeEnabled') ?? false;
}
@ -208,12 +201,12 @@ class ManagerAPI {
await _prefs.setBool('useDynamicTheme', value);
}
bool getUseDarkTheme() {
return _prefs.getBool('useDarkTheme') ?? false;
int getThemeMode() {
return _prefs.getInt('themeMode') ?? 2;
}
Future<void> setUseDarkTheme(bool value) async {
await _prefs.setBool('useDarkTheme', value);
Future<void> setThemeMode(int value) async {
await _prefs.setInt('themeMode', value);
}
bool areUniversalPatchesEnabled() {
@ -311,28 +304,46 @@ class ManagerAPI {
}
Future<List<Patch>> getPatches() async {
try {
final String repoName = getPatchesRepo();
final String currentVersion = await getCurrentPatchesVersion();
final String url = getPatchesDownloadURL(false);
return await _githubAPI.getPatches(
repoName,
currentVersion,
url,
);
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
return [];
if (patches.isNotEmpty) {
return patches;
}
final File? patchBundleFile = await downloadPatches();
final Directory appCache = await getTemporaryDirectory();
Directory('${appCache.path}/cache').createSync();
final Directory workDir =
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(
'getPatches',
{
'patchBundleFilePath': patchBundleFile.path,
'cacheDirPath': cacheDir.path,
},
);
final List<dynamic> patchesJsonList = jsonDecode(patchesJson);
patches = patchesJsonList
.map((patchJson) => Patch.fromJson(patchJson))
.toList();
return patches;
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
}
}
return List.empty();
}
Future<File?> downloadPatches() async {
try {
final String repoName = getPatchesRepo();
final String currentVersion = await getCurrentPatchesVersion();
final String url = getPatchesDownloadURL(true);
final String url = getPatchesDownloadURL();
return await _githubAPI.getPatchesReleaseFile(
'.jar',
repoName,
@ -458,8 +469,7 @@ class ManagerAPI {
Future<void> setCurrentPatchesVersion(String version) async {
await _prefs.setString('patchesVersion', version);
await setPatchesDownloadURL('', false);
await setPatchesDownloadURL('', true);
await setPatchesDownloadURL('');
await downloadPatches();
}
@ -495,62 +505,33 @@ class ManagerAPI {
return toRemove;
}
Future<List<PatchedApplication>> getUnsavedApps(
List<PatchedApplication> patchedApps,
) async {
final List<PatchedApplication> unsavedApps = [];
Future<List<PatchedApplication>> getMountedApps() async {
final List<PatchedApplication> mountedApps = [];
final bool hasRootPermissions = await _rootAPI.hasRootPermissions();
if (hasRootPermissions) {
final List<String> installedApps = await _rootAPI.getInstalledApps();
for (final String packageName in installedApps) {
if (!patchedApps.any((app) => app.packageName == packageName)) {
final ApplicationWithIcon? application = await DeviceApps.getApp(
packageName,
true,
) as ApplicationWithIcon?;
if (application != null) {
unsavedApps.add(
PatchedApplication(
name: application.appName,
packageName: application.packageName,
originalPackageName: application.packageName,
version: application.versionName!,
apkFilePath: application.apkFilePath,
icon: application.icon,
patchDate: DateTime.now(),
isRooted: true,
),
);
}
}
}
}
final List<Application> userApps =
await DeviceApps.getInstalledApplications();
for (final Application app in userApps) {
if (app.packageName.startsWith('app.revanced') &&
!app.packageName.startsWith('app.revanced.manager.') &&
!patchedApps.any((uapp) => uapp.packageName == app.packageName)) {
final ApplicationWithIcon? application = await DeviceApps.getApp(
app.packageName,
packageName,
true,
) as ApplicationWithIcon?;
if (application != null) {
unsavedApps.add(
mountedApps.add(
PatchedApplication(
name: application.appName,
packageName: application.packageName,
originalPackageName: application.packageName,
version: application.versionName!,
apkFilePath: application.apkFilePath,
icon: application.icon,
patchDate: DateTime.now(),
isRooted: true,
),
);
}
}
}
return unsavedApps;
return mountedApps;
}
Future<void> showPatchesChangeWarningDialog(BuildContext context) {
@ -612,34 +593,20 @@ class ManagerAPI {
Future<void> reAssessSavedApps() async {
final List<PatchedApplication> patchedApps = getPatchedApps();
final List<PatchedApplication> unsavedApps =
await getUnsavedApps(patchedApps);
patchedApps.addAll(unsavedApps);
// Remove apps that are not installed anymore.
final List<PatchedApplication> toRemove =
await getAppsToRemove(patchedApps);
await getAppsToRemove(patchedApps);
patchedApps.removeWhere((a) => toRemove.contains(a));
for (final PatchedApplication app in patchedApps) {
app.hasUpdates =
await hasAppUpdates(app.originalPackageName, app.patchDate);
app.changelog =
await getAppChangelog(app.originalPackageName, app.patchDate);
if (!app.hasUpdates) {
final String? currentInstalledVersion =
(await DeviceApps.getApp(app.packageName))?.versionName;
if (currentInstalledVersion != null) {
final String currentSavedVersion = app.version;
final int currentInstalledVersionInt = int.parse(
currentInstalledVersion.replaceAll(RegExp('[^0-9]'), ''),
);
final int currentSavedVersionInt = int.parse(
currentSavedVersion.replaceAll(RegExp('[^0-9]'), ''),
);
if (currentInstalledVersionInt > currentSavedVersionInt) {
app.hasUpdates = true;
}
}
}
}
// Determine all apps that are installed by mounting.
final List<PatchedApplication> mountedApps = await getMountedApps();
mountedApps.removeWhere(
(app) => patchedApps
.any((patchedApp) => patchedApp.packageName == app.packageName),
);
patchedApps.addAll(mountedApps);
await setPatchedApps(patchedApps);
}
@ -656,37 +623,6 @@ class ManagerAPI {
return !existsNonRoot;
}
Future<bool> hasAppUpdates(
String packageName,
DateTime patchDate,
) async {
final List<String> commits = await _githubAPI.getCommits(
packageName,
getPatchesRepo(),
patchDate,
);
return commits.isNotEmpty;
}
Future<List<String>> getAppChangelog(
String packageName,
DateTime patchDate,
) async {
List<String> newCommits = await _githubAPI.getCommits(
packageName,
getPatchesRepo(),
patchDate,
);
if (newCommits.isEmpty) {
newCommits = await _githubAPI.getCommits(
packageName,
getPatchesRepo(),
patchDate,
);
}
return newCommits;
}
Future<bool> isSplitApk(PatchedApplication patchedApp) async {
Application? app;
if (patchedApp.isFromStorage) {
@ -754,6 +690,8 @@ class ManagerAPI {
Future<void> resetLastSelectedPatches() async {
final File selectedPatchesFile = File(storedPatchesFile);
selectedPatchesFile.deleteSync();
if (selectedPatchesFile.existsSync()) {
selectedPatchesFile.deleteSync();
}
}
}

View File

@ -25,12 +25,13 @@ class PatcherAPI {
late Directory _tmpDir;
late File _keyStoreFile;
List<Patch> _patches = [];
List<Patch> _universalPatches = [];
List<String> _compatiblePackages = [];
Map filteredPatches = <String, List<Patch>>{};
File? _outFile;
File? outFile;
Future<void> initialize() async {
await _loadPatches();
await _managerAPI.downloadPatches();
await loadPatches();
await _managerAPI.downloadIntegrations();
final Directory appCache = await getTemporaryDirectory();
_dataDir = await getExternalStorageDirectory() ?? appCache;
@ -45,7 +46,23 @@ class PatcherAPI {
}
}
Future<void> _loadPatches() async {
List<String> getCompatiblePackages() {
final List<String> compatiblePackages = [];
for (final Patch patch in _patches) {
for (final Package package in patch.compatiblePackages) {
if (!compatiblePackages.contains(package.name)) {
compatiblePackages.add(package.name);
}
}
}
return compatiblePackages;
}
List<Patch> getUniversalPatches() {
return _patches.where((patch) => patch.compatiblePackages.isEmpty).toList();
}
Future<void> loadPatches() async {
try {
if (_patches.isEmpty) {
_patches = await _managerAPI.getPatches();
@ -56,6 +73,9 @@ class PatcherAPI {
}
_patches = List.empty();
}
_compatiblePackages = getCompatiblePackages();
_universalPatches = getUniversalPatches();
}
Future<List<ApplicationWithIcon>> getFilteredInstalledApps(
@ -63,48 +83,42 @@ class PatcherAPI {
) async {
final List<ApplicationWithIcon> filteredApps = [];
final bool allAppsIncluded =
_patches.any((patch) => patch.compatiblePackages.isEmpty) &&
showUniversalPatches;
_universalPatches.isNotEmpty && showUniversalPatches;
if (allAppsIncluded) {
final allPackages = await DeviceApps.getInstalledApplications(
final appList = await DeviceApps.getInstalledApplications(
includeAppIcons: true,
onlyAppsWithLaunchIntent: true,
);
for (final pkg in allPackages) {
if (!filteredApps.any((app) => app.packageName == pkg.packageName)) {
final appInfo = await DeviceApps.getApp(
pkg.packageName,
true,
) as ApplicationWithIcon?;
if (appInfo != null) {
filteredApps.add(appInfo);
}
}
for (final app in appList) {
filteredApps.add(app as ApplicationWithIcon);
}
}
for (final Patch patch in _patches) {
for (final Package package in patch.compatiblePackages) {
try {
if (!filteredApps.any((app) => app.packageName == package.name)) {
final ApplicationWithIcon? app = await DeviceApps.getApp(
package.name,
true,
) as ApplicationWithIcon?;
if (app != null) {
filteredApps.add(app);
}
}
} on Exception catch (e) {
if (kDebugMode) {
print(e);
for (final packageName in _compatiblePackages) {
try {
if (!filteredApps.any((app) => app.packageName == packageName)) {
final ApplicationWithIcon? app = await DeviceApps.getApp(
packageName,
true,
) as ApplicationWithIcon?;
if (app != null) {
filteredApps.add(app);
}
}
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
}
}
return filteredApps;
}
List<Patch> getFilteredPatches(String packageName) {
if (!_compatiblePackages.contains(packageName)) {
return _universalPatches;
}
final List<Patch> patches = _patches
.where(
(patch) =>
@ -132,55 +146,20 @@ class PatcherAPI {
.toList();
}
Future<bool> needsResourcePatching(
List<Patch> selectedPatches,
) async {
return selectedPatches.any(
(patch) => patch.dependencies.any(
(dep) => dep.contains('resource-'),
),
);
}
Future<bool> needsSettingsPatch(List<Patch> selectedPatches) async {
return selectedPatches.any(
(patch) => patch.dependencies.any(
(dep) => dep.contains('settings'),
),
);
}
Future<void> runPatcher(
String packageName,
String apkFilePath,
List<Patch> selectedPatches,
) async {
final bool includeSettings = await needsSettingsPatch(selectedPatches);
if (includeSettings) {
try {
final Patch? settingsPatch = _patches.firstWhereOrNull(
(patch) =>
patch.name.contains('settings') &&
patch.compatiblePackages.any((pack) => pack.name == packageName),
);
if (settingsPatch != null) {
selectedPatches.add(settingsPatch);
}
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
}
}
final File? patchBundleFile = await _managerAPI.downloadPatches();
final File? integrationsFile = await _managerAPI.downloadIntegrations();
if (patchBundleFile != null) {
if (integrationsFile != null) {
_dataDir.createSync();
_tmpDir.createSync();
final Directory workDir = _tmpDir.createTempSync('tmp-');
final File inputFile = File('${workDir.path}/base.apk');
final File patchedFile = File('${workDir.path}/patched.apk');
_outFile = File('${workDir.path}/out.apk');
outFile = File('${workDir.path}/out.apk');
final Directory cacheDir = Directory('${workDir.path}/cache');
cacheDir.createSync();
final String originalFilePath = apkFilePath;
@ -188,12 +167,11 @@ class PatcherAPI {
await patcherChannel.invokeMethod(
'runPatcher',
{
'patchBundleFilePath': patchBundleFile.path,
'originalFilePath': originalFilePath,
'inputFilePath': inputFile.path,
'patchedFilePath': patchedFile.path,
'outFilePath': _outFile!.path,
'integrationsPath': integrationsFile!.path,
'outFilePath': outFile!.path,
'integrationsPath': integrationsFile.path,
'selectedPatches': selectedPatches.map((p) => p.name).toList(),
'cacheDirPath': cacheDir.path,
'keyStoreFilePath': _keyStoreFile.path,
@ -219,7 +197,7 @@ class PatcherAPI {
}
Future<bool> installPatchedFile(PatchedApplication patchedApp) async {
if (_outFile != null) {
if (outFile != null) {
try {
if (patchedApp.isRooted) {
final bool hasRootPermissions = await _rootAPI.hasRootPermissions();
@ -227,11 +205,11 @@ class PatcherAPI {
return _rootAPI.installApp(
patchedApp.packageName,
patchedApp.apkFilePath,
_outFile!.path,
outFile!.path,
);
}
} else {
final install = await InstallPlugin.installApk(_outFile!.path);
final install = await InstallPlugin.installApk(outFile!.path);
return install['isSuccess'];
}
} on Exception catch (e) {
@ -246,11 +224,11 @@ class PatcherAPI {
void exportPatchedFile(String appName, String version) {
try {
if (_outFile != null) {
if (outFile != null) {
final String newName = _getFileName(appName, version);
CRFileSaver.saveFileWithDialog(
SaveFileDialogParams(
sourceFilePath: _outFile!.path,
sourceFilePath: outFile!.path,
destinationFileName: newName,
),
);
@ -264,12 +242,12 @@ class PatcherAPI {
void sharePatchedFile(String appName, String version) {
try {
if (_outFile != null) {
if (outFile != null) {
final String newName = _getFileName(appName, version);
final int lastSeparator = _outFile!.path.lastIndexOf('/');
final int lastSeparator = outFile!.path.lastIndexOf('/');
final String newPath =
_outFile!.path.substring(0, lastSeparator + 1) + newName;
final File shareFile = _outFile!.copySync(newPath);
outFile!.path.substring(0, lastSeparator + 1) + newName;
final File shareFile = outFile!.copySync(newPath);
ShareExtend.share(shareFile.path, 'file');
}
} on Exception catch (e) {

View File

@ -7,12 +7,15 @@ 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:synchronized/synchronized.dart';
import 'package:timeago/timeago.dart';
@lazySingleton
class RevancedAPI {
late Dio _dio = Dio();
final Lock getToolsLock = Lock();
final _cacheOptions = CacheOptions(
store: MemCacheStore(),
maxStale: const Duration(days: 1),
@ -66,21 +69,23 @@ class RevancedAPI {
Future<Map<String, dynamic>?> _getLatestRelease(
String extension,
String repoName,
) async {
try {
final response = await _dio.get('/tools');
final List<dynamic> tools = response.data['tools'];
return tools.firstWhereOrNull(
(t) =>
t['repository'] == repoName &&
(t['name'] as String).endsWith(extension),
);
} on Exception catch (e) {
if (kDebugMode) {
print(e);
) {
return getToolsLock.synchronized(() async {
try {
final response = await _dio.get('/tools');
final List<dynamic> tools = response.data['tools'];
return tools.firstWhereOrNull(
(t) =>
t['repository'] == repoName &&
(t['name'] as String).endsWith(extension),
);
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
return null;
}
return null;
}
});
}
Future<String?> getLatestReleaseVersion(

View File

@ -1,12 +1,16 @@
import 'dart:ui';
import 'package:dynamic_color/dynamic_color.dart';
import 'package:dynamic_themes/dynamic_themes.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/app/app.router.dart';
import 'package:revanced_manager/services/manager_api.dart';
import 'package:revanced_manager/theme.dart';
import 'package:stacked_services/stacked_services.dart';
class DynamicThemeBuilder extends StatelessWidget {
class DynamicThemeBuilder extends StatefulWidget {
const DynamicThemeBuilder({
Key? key,
required this.title,
@ -17,6 +21,35 @@ class DynamicThemeBuilder extends StatelessWidget {
final Widget home;
final Iterable<LocalizationsDelegate> localizationsDelegates;
@override
State<DynamicThemeBuilder> createState() => _DynamicThemeBuilderState();
}
class _DynamicThemeBuilderState extends State<DynamicThemeBuilder> with WidgetsBindingObserver {
Brightness brightness = PlatformDispatcher.instance.platformBrightness;
final ManagerAPI _managerAPI = locator<ManagerAPI>();
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void didChangePlatformBrightness() {
setState(() {
brightness = PlatformDispatcher.instance.platformBrightness;
});
if (_managerAPI.getThemeMode() < 2) {
SystemChrome.setSystemUIOverlayStyle(
SystemUiOverlayStyle(
systemNavigationBarIconBrightness:
brightness == Brightness.light ? Brightness.dark : Brightness.light,
),
);
}
}
@override
Widget build(BuildContext context) {
return DynamicColorBuilder(
@ -50,24 +83,32 @@ class DynamicThemeBuilder extends StatelessWidget {
return DynamicTheme(
themeCollection: ThemeCollection(
themes: {
0: lightCustomTheme,
1: darkCustomTheme,
2: lightDynamicTheme,
3: darkDynamicTheme,
0: brightness == Brightness.light ? lightCustomTheme : darkCustomTheme,
1: brightness == Brightness.light ? lightDynamicTheme : darkDynamicTheme,
2: lightCustomTheme,
3: lightDynamicTheme,
4: darkCustomTheme,
5: darkDynamicTheme,
},
fallbackTheme: lightCustomTheme,
fallbackTheme: PlatformDispatcher.instance.platformBrightness == Brightness.light ? lightCustomTheme : darkCustomTheme,
),
builder: (context, theme) => MaterialApp(
debugShowCheckedModeBanner: false,
title: title,
navigatorKey: StackedService.navigatorKey,
onGenerateRoute: StackedRouter().onGenerateRoute,
theme: theme,
home: home,
localizationsDelegates: localizationsDelegates,
),
debugShowCheckedModeBanner: false,
title: widget.title,
navigatorKey: StackedService.navigatorKey,
onGenerateRoute: StackedRouter().onGenerateRoute,
theme: theme,
home: widget.home,
localizationsDelegates: widget.localizationsDelegates,
),
);
},
);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
}

View File

@ -54,7 +54,7 @@ class _AppSelectorViewState extends State<AppSelectorView> {
onPressed: () => Navigator.of(context).pop(),
),
bottom: PreferredSize(
preferredSize: const Size.fromHeight(64.0),
preferredSize: const Size.fromHeight(66.0),
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 8.0,

View File

@ -73,7 +73,6 @@ class AppSelectorViewModel extends BaseViewModel {
locator<PatcherViewModel>().selectedApp = PatchedApplication(
name: application.appName,
packageName: application.packageName,
originalPackageName: application.packageName,
version: application.versionName!,
apkFilePath: application.apkFilePath,
icon: application.icon,
@ -90,10 +89,14 @@ class AppSelectorViewModel extends BaseViewModel {
await DeviceApps.getApp(packageName, true) as ApplicationWithIcon?;
if (app != null) {
if (await checkSplitApk(packageName) && !isRooted) {
return showSelectFromStorageDialog(context);
if (context.mounted) {
return showSelectFromStorageDialog(context);
}
} else if (!await checkSplitApk(packageName) || isRooted) {
selectApp(app);
Navigator.pop(context);
if (context.mounted) {
Navigator.pop(context);
}
}
}
}
@ -198,7 +201,6 @@ class AppSelectorViewModel extends BaseViewModel {
locator<PatcherViewModel>().selectedApp = PatchedApplication(
name: application.appName,
packageName: application.packageName,
originalPackageName: application.packageName,
version: application.versionName!,
apkFilePath: result.files.single.path!,
icon: application.icon,

View File

@ -37,7 +37,6 @@ class HomeViewModel extends BaseViewModel {
DateTime? _lastUpdate;
bool showUpdatableApps = false;
List<PatchedApplication> patchedInstalledApps = [];
List<PatchedApplication> patchedUpdatableApps = [];
String? _latestManagerVersion = '';
File? downloadedApk;
@ -82,7 +81,7 @@ class HomeViewModel extends BaseViewModel {
_toast.showBottom('homeView.errorDownloadMessage');
}
}
_getPatchedApps();
_managerAPI.reAssessSavedApps().then((_) => _getPatchedApps());
}
@ -108,10 +107,6 @@ class HomeViewModel extends BaseViewModel {
void _getPatchedApps() {
patchedInstalledApps = _managerAPI.getPatchedApps().toList();
patchedUpdatableApps = _managerAPI
.getPatchedApps()
.where((app) => app.hasUpdates == true)
.toList();
notifyListeners();
}
@ -469,11 +464,7 @@ class HomeViewModel extends BaseViewModel {
}
Future<void> forceRefresh(BuildContext context) async {
await Future.delayed(const Duration(seconds: 1));
if (_lastUpdate == null ||
_lastUpdate!.difference(DateTime.now()).inSeconds > 2) {
_managerAPI.clearAllData();
}
_managerAPI.clearAllData();
_toast.showBottom('homeView.refreshSuccess');
initialize(context);
}

View File

@ -21,11 +21,23 @@ class InstallerView extends StatelessWidget {
bottom: false,
child: Scaffold(
floatingActionButton: Visibility(
visible: !model.isPatching,
visible: !model.isPatching && !model.hasErrors,
child: FloatingActionButton.extended(
label: I18nText('installerView.installButton'),
icon: const Icon(Icons.file_download_outlined),
onPressed: () => model.installTypeDialog(context),
label: I18nText(
model.isInstalled
? 'installerView.openButton'
: 'installerView.installButton',
),
icon: model.isInstalled
? const Icon(Icons.open_in_new)
: const Icon(Icons.file_download_outlined),
onPressed: model.isInstalled
? () => {
model.openApp(),
model.cleanPatcher(),
Navigator.of(context).pop(),
}
: () => model.installTypeDialog(context),
elevation: 0,
),
),

View File

@ -130,28 +130,28 @@ class InstallerViewModel extends BaseViewModel {
Future<void> runPatcher() async {
try {
update(0.0, 'Initializing...', 'Initializing installer');
if (_patches.isNotEmpty) {
try {
update(0.1, '', 'Creating working directory');
await _patcherAPI.runPatcher(
_app.packageName,
_app.apkFilePath,
_patches,
);
} on Exception catch (e) {
update(
-100.0,
'Aborted...',
'An error occurred! Aborted\nError:\n$e',
);
if (kDebugMode) {
print(e);
}
}
} else {
update(-100.0, 'Aborted...', 'No app or patches selected! Aborted');
await _patcherAPI.runPatcher(
_app.packageName,
_app.apkFilePath,
_patches,
);
} on Exception catch (e) {
update(
-100.0,
'Failed...',
'Something went wrong:\n$e',
);
if (kDebugMode) {
print(e);
}
}
// Necessary to reset the state of patches by reloading them
// in a later patching process.
_managerAPI.patches.clear();
await _patcherAPI.loadPatches();
try {
if (FlutterBackground.isBackgroundExecutionEnabled) {
try {
FlutterBackground.disableBackgroundExecution();
@ -182,52 +182,55 @@ class InstallerViewModel extends BaseViewModel {
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
icon: const Icon(Icons.file_download_outlined),
contentPadding: const EdgeInsets.symmetric(vertical: 16),
content: ValueListenableBuilder(
valueListenable: installType,
builder: (context, value, child) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 10,
),
child: I18nText(
'installerView.installTypeDescription',
child: Text(
'',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.secondary,
content: SingleChildScrollView(
child: ValueListenableBuilder(
valueListenable: installType,
builder: (context, value, child) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 10,
),
child: I18nText(
'installerView.installTypeDescription',
child: Text(
'',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.secondary,
),
),
),
),
),
RadioListTile(
title: I18nText('installerView.installNonRootType'),
subtitle: I18nText('installerView.installRecommendedType'),
contentPadding: const EdgeInsets.symmetric(horizontal: 10),
value: 0,
groupValue: value,
onChanged: (selected) {
installType.value = selected!;
},
),
RadioListTile(
title: I18nText('installerView.installRootType'),
contentPadding: const EdgeInsets.symmetric(horizontal: 10),
value: 1,
groupValue: value,
onChanged: (selected) {
installType.value = selected!;
},
),
],
);
},
RadioListTile(
title: I18nText('installerView.installNonRootType'),
contentPadding:
const EdgeInsets.symmetric(horizontal: 16),
value: 0,
groupValue: value,
onChanged: (selected) {
installType.value = selected!;
},
),
RadioListTile(
title: I18nText('installerView.installRootType'),
contentPadding:
const EdgeInsets.symmetric(horizontal: 16),
value: 1,
groupValue: value,
onChanged: (selected) {
installType.value = selected!;
},
),
],
);
},
),
),
actions: [
CustomMaterialButton(
@ -255,9 +258,9 @@ class InstallerViewModel extends BaseViewModel {
Future<void> stopPatcher() async {
try {
isCanceled = true;
update(0.5, 'Aborting...', 'Canceling patching process');
update(0.5, 'Canceling...', 'Canceling patching process');
await _patcherAPI.stopPatcher();
update(-100.0, 'Aborted...', 'Press back to exit');
update(-100.0, 'Canceled...', 'Press back to exit');
} on Exception catch (e) {
if (kDebugMode) {
print(e);
@ -268,56 +271,33 @@ class InstallerViewModel extends BaseViewModel {
Future<void> installResult(BuildContext context, bool installAsRoot) async {
try {
_app.isRooted = installAsRoot;
final bool hasMicroG =
_patches.any((p) => p.name.endsWith('MicroG support'));
final bool rootMicroG = installAsRoot && hasMicroG;
final bool rootFromStorage = installAsRoot && _app.isFromStorage;
final bool ytWithoutRootMicroG =
!installAsRoot && !hasMicroG && _app.packageName.contains('youtube');
if (rootMicroG || rootFromStorage || ytWithoutRootMicroG) {
return showDialog(
context: context,
builder: (context) => AlertDialog(
title: I18nText('installerView.installErrorDialogTitle'),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
content: I18nText(
rootMicroG
? 'installerView.installErrorDialogText1'
: rootFromStorage
? 'installerView.installErrorDialogText3'
: 'installerView.installErrorDialogText2',
),
actions: <Widget>[
CustomMaterialButton(
label: I18nText('okButton'),
onPressed: () => Navigator.of(context).pop(),
),
],
),
);
} else {
update(
1.0,
'Installing...',
_app.isRooted
? 'Installing patched file using root method'
: 'Installing patched file using nonroot method',
);
isInstalled = await _patcherAPI.installPatchedFile(_app);
if (isInstalled) {
update(1.0, 'Installed!', 'Installed!');
_app.isFromStorage = false;
_app.patchDate = DateTime.now();
_app.appliedPatches = _patches.map((p) => p.name).toList();
if (hasMicroG) {
_app.name += ' ReVanced';
_app.packageName = _app.packageName.replaceFirst(
'com.google.',
'app.revanced.',
);
}
await _managerAPI.savePatchedApp(_app);
update(
1.0,
'Installing...',
_app.isRooted
? 'Installing patched file using root method'
: 'Installing patched file using nonroot method',
);
isInstalled = await _patcherAPI.installPatchedFile(_app);
if (isInstalled) {
_app.isFromStorage = false;
_app.patchDate = DateTime.now();
_app.appliedPatches = _patches.map((p) => p.name).toList();
// In case a patch changed the app name or package name,
// update the app info.
final app =
await DeviceApps.getAppFromStorage(_patcherAPI.outFile!.path);
if (app != null) {
_app.name = app.appName;
_app.packageName = app.packageName;
}
await _managerAPI.savePatchedApp(_app);
update(1.0, 'Installed!', 'Installed!');
} else {
// TODO(aabed): Show error message.
}
} on Exception catch (e) {
if (kDebugMode) {

View File

@ -1,4 +1,5 @@
// ignore_for_file: use_build_context_synchronously
import 'package:device_info_plus/device_info_plus.dart';
import 'package:dynamic_themes/dynamic_themes.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@ -18,25 +19,35 @@ class NavigationViewModel extends IndexTrackingViewModel {
Future<void> initialize(BuildContext context) async {
locator<Toast>().initialize(context);
final SharedPreferences prefs = await SharedPreferences.getInstance();
requestManageExternalStorage();
await requestManageExternalStorage();
if (prefs.getBool('permissionsRequested') == null) {
await Permission.storage.request();
await Permission.manageExternalStorage.request();
await prefs.setBool('permissionsRequested', true);
RootAPI().hasRootPermissions().then(
await RootAPI().hasRootPermissions().then(
(value) => Permission.requestInstallPackages.request().then(
(value) => Permission.ignoreBatteryOptimizations.request(),
),
);
}
if (prefs.getBool('useDarkTheme') == null) {
final bool isDark =
MediaQuery.platformBrightnessOf(context) != Brightness.light;
await prefs.setBool('useDarkTheme', isDark);
await DynamicTheme.of(context)!.setTheme(isDark ? 1 : 0);
final dynamicTheme = DynamicTheme.of(context)!;
if (prefs.getInt('themeMode') == null) {
await prefs.setInt('themeMode', 0);
await dynamicTheme.setTheme(0);
}
// Force disable Material You on Android 11 and below
if (dynamicTheme.themeId.isOdd) {
const int android12SdkVersion = 31;
final AndroidDeviceInfo info = await DeviceInfoPlugin().androidInfo;
if (info.version.sdkInt < android12SdkVersion) {
await prefs.setInt('themeMode', 0);
await prefs.setBool('useDynamicTheme', false);
await dynamicTheme.setTheme(0);
}
}
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
SystemChrome.setSystemUIOverlayStyle(
SystemUiOverlayStyle(

View File

@ -44,49 +44,6 @@ class PatcherViewModel extends BaseViewModel {
return selectedApp == null;
}
Future<bool> isValidPatchConfig() async {
final bool needsResourcePatching = await _patcherAPI.needsResourcePatching(
selectedPatches,
);
if (needsResourcePatching && selectedApp != null) {
final bool isSplit = await _managerAPI.isSplitApk(selectedApp!);
return !isSplit;
}
return true;
}
Future<void> showPatchConfirmationDialog(BuildContext context) async {
final bool isValid = await isValidPatchConfig();
if (context.mounted) {
if (isValid) {
showArmv7WarningDialog(context);
} else {
return showDialog(
context: context,
builder: (context) => AlertDialog(
title: I18nText('warning'),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
content: I18nText('patcherView.splitApkWarningDialogText'),
actions: <Widget>[
CustomMaterialButton(
label: I18nText('noButton'),
onPressed: () => Navigator.of(context).pop(),
),
CustomMaterialButton(
label: I18nText('yesButton'),
isFilled: false,
onPressed: () {
Navigator.of(context).pop();
showArmv7WarningDialog(context);
},
),
],
),
);
}
}
}
Future<void> showRemovedPatchesDialog(BuildContext context) async {
if (removedPatches.isNotEmpty) {
return showDialog(
@ -115,7 +72,7 @@ class PatcherViewModel extends BaseViewModel {
),
);
} else {
showArmv7WarningDialog(context);
showArmv7WarningDialog(context); // TODO(aabed): Find out why this is here
}
}
@ -185,9 +142,9 @@ class PatcherViewModel extends BaseViewModel {
this.selectedPatches.clear();
removedPatches.clear();
final List<String> selectedPatches =
await _managerAPI.getSelectedPatches(selectedApp!.originalPackageName);
await _managerAPI.getSelectedPatches(selectedApp!.packageName);
final List<Patch> patches =
_patcherAPI.getFilteredPatches(selectedApp!.originalPackageName);
_patcherAPI.getFilteredPatches(selectedApp!.packageName);
this
.selectedPatches
.addAll(patches.where((patch) => selectedPatches.contains(patch.name)));
@ -203,7 +160,7 @@ class PatcherViewModel extends BaseViewModel {
.selectedPatches
.removeWhere((patch) => patch.compatiblePackages.isEmpty);
}
final usedPatches = _managerAPI.getUsedPatches(selectedApp!.originalPackageName);
final usedPatches = _managerAPI.getUsedPatches(selectedApp!.packageName);
for (final patch in usedPatches){
if (!patches.any((p) => p.name == patch.name)){
removedPatches.add('\u2022 ${patch.name}');

View File

@ -81,7 +81,7 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
child: Container(
margin: const EdgeInsets.only(top: 12, bottom: 12),
padding:
const EdgeInsets.symmetric(horizontal: 6, vertical: 6),
const EdgeInsets.symmetric(horizontal: 6, vertical: 6),
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
@ -99,7 +99,7 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
),
CustomPopupMenu(
onSelected: (value) =>
{model.onMenuSelection(value, context)},
{model.onMenuSelection(value, context)},
children: {
0: I18nText(
'patchesSelectorView.loadPatchesSelection',
@ -114,7 +114,7 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
),
],
bottom: PreferredSize(
preferredSize: const Size.fromHeight(64.0),
preferredSize: const Size.fromHeight(66.0),
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 8.0,
@ -194,12 +194,13 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
return PatchItem(
name: patch.name,
simpleName: patch.getSimpleName(),
description: patch.description,
description: patch.description ?? '',
packageVersion: model.getAppInfo().version,
supportedPackageVersions:
model.getSupportedVersions(patch),
isUnsupported: !isPatchSupported(patch),
isChangeEnabled: _managerAPI.isPatchesChangeEnabled(),
isChangeEnabled:
_managerAPI.isPatchesChangeEnabled(),
isNew: model.isPatchNew(
patch,
model.getAppInfo().packageName,
@ -221,8 +222,23 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
padding: const EdgeInsets.symmetric(
vertical: 10.0,
),
child: I18nText(
'patchesSelectorView.universalPatches',
child: Container(
padding: const EdgeInsets.only(
top: 10.0,
bottom: 10.0,
left: 5.0,
),
child: I18nText(
'patchesSelectorView.universalPatches',
child: Text(
'',
style: TextStyle(
color: Theme.of(context)
.colorScheme
.primary,
),
),
),
),
),
...model.getQueriedPatches(_query).map((patch) {
@ -230,13 +246,14 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
return PatchItem(
name: patch.name,
simpleName: patch.getSimpleName(),
description: patch.description,
description: patch.description ?? '',
packageVersion:
model.getAppInfo().version,
supportedPackageVersions:
model.getSupportedVersions(patch),
isUnsupported: !isPatchSupported(patch),
isChangeEnabled: _managerAPI.isPatchesChangeEnabled(),
isChangeEnabled:
_managerAPI.isPatchesChangeEnabled(),
isNew: false,
isSelected: model.isSelected(patch),
onChanged: (value) => model.selectPatch(

View File

@ -28,7 +28,7 @@ class PatchesSelectorViewModel extends BaseViewModel {
getPatchesVersion().whenComplete(() => notifyListeners());
patches.addAll(
_patcherAPI.getFilteredPatches(
selectedApp!.originalPackageName,
selectedApp!.packageName,
),
);
patches.sort((a, b) {
@ -98,11 +98,11 @@ class PatchesSelectorViewModel extends BaseViewModel {
void selectDefaultPatches() {
selectedPatches.clear();
if (locator<PatcherViewModel>().selectedApp?.originalPackageName != null) {
if (locator<PatcherViewModel>().selectedApp?.packageName != null) {
selectedPatches.addAll(
_patcherAPI
.getFilteredPatches(
locator<PatcherViewModel>().selectedApp!.originalPackageName,
locator<PatcherViewModel>().selectedApp!.packageName,
)
.where(
(element) =>
@ -187,7 +187,7 @@ class PatchesSelectorViewModel extends BaseViewModel {
final List<String> selectedPatches =
this.selectedPatches.map((patch) => patch.name).toList();
await _managerAPI.setSelectedPatches(
locator<PatcherViewModel>().selectedApp!.originalPackageName,
locator<PatcherViewModel>().selectedApp!.packageName,
selectedPatches,
);
}
@ -195,7 +195,7 @@ class PatchesSelectorViewModel extends BaseViewModel {
Future<void> loadSelectedPatches(BuildContext context) async {
if (_managerAPI.isPatchesChangeEnabled()) {
final List<String> selectedPatches = await _managerAPI.getSelectedPatches(
locator<PatcherViewModel>().selectedApp!.originalPackageName,
locator<PatcherViewModel>().selectedApp!.packageName,
);
if (selectedPatches.isNotEmpty) {
this.selectedPatches.clear();

View File

@ -8,6 +8,7 @@ import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/services/manager_api.dart';
import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_section.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
import 'package:stacked/stacked.dart';
final _settingViewModel = SettingsViewModel();
@ -24,37 +25,114 @@ class SUpdateTheme extends BaseViewModel {
Future<void> setUseDynamicTheme(BuildContext context, bool value) async {
await _managerAPI.setUseDynamicTheme(value);
final int currentTheme = DynamicTheme.of(context)!.themeId;
if (currentTheme.isEven) {
await DynamicTheme.of(context)!.setTheme(value ? 2 : 0);
} else {
await DynamicTheme.of(context)!.setTheme(value ? 3 : 1);
}
final int currentTheme = (DynamicTheme.of(context)!.themeId ~/ 2) * 2;
await DynamicTheme.of(context)!.setTheme(currentTheme + (value ? 1 : 0));
notifyListeners();
}
bool getDarkThemeStatus() {
return _managerAPI.getUseDarkTheme();
int getThemeMode() {
return _managerAPI.getThemeMode();
}
Future<void> setUseDarkTheme(BuildContext context, bool value) async {
await _managerAPI.setUseDarkTheme(value);
final int currentTheme = DynamicTheme.of(context)!.themeId;
if (currentTheme < 2) {
await DynamicTheme.of(context)!.setTheme(value ? 1 : 0);
} else {
await DynamicTheme.of(context)!.setTheme(value ? 3 : 2);
}
Future<void> setThemeMode(BuildContext context, int value) async {
await _managerAPI.setThemeMode(value);
final bool isDynamicTheme = DynamicTheme.of(context)!.themeId.isEven;
await DynamicTheme.of(context)!.setTheme(value * 2 + (isDynamicTheme ? 0 : 1));
final bool isLight = value != 2 && (value == 1 || DynamicTheme.of(context)!.theme.brightness == Brightness.light);
SystemChrome.setSystemUIOverlayStyle(
SystemUiOverlayStyle(
systemNavigationBarIconBrightness:
value ? Brightness.light : Brightness.dark,
isLight ? Brightness.dark : Brightness.light,
),
);
notifyListeners();
}
I18nText getThemeModeName() {
switch (getThemeMode()) {
case 0:
return I18nText('settingsView.systemThemeLabel');
case 1:
return I18nText('settingsView.lightThemeLabel');
case 2:
return I18nText('settingsView.darkThemeLabel');
default:
return I18nText('settingsView.systemThemeLabel');
}
}
Future<void> showThemeDialog(BuildContext context) async {
final ValueNotifier<int> newTheme = ValueNotifier(getThemeMode());
return showDialog(
context: context,
builder: (context) => AlertDialog(
title: I18nText('settingsView.themeModeLabel'),
icon: const Icon(Icons.palette),
contentPadding: const EdgeInsets.symmetric(vertical: 16),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
content: SingleChildScrollView(
child: ValueListenableBuilder(
valueListenable: newTheme,
builder: (context, value, child) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
RadioListTile(
title: I18nText('settingsView.systemThemeLabel'),
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
value: 0,
groupValue: value,
onChanged: (value) {
newTheme.value = value!;
},
),
RadioListTile(
title: I18nText('settingsView.lightThemeLabel'),
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
value: 1,
groupValue: value,
onChanged: (value) {
newTheme.value = value!;
},
),
RadioListTile(
title: I18nText('settingsView.darkThemeLabel'),
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
value: 2,
groupValue: value,
onChanged: (value) {
newTheme.value = value!;
},
),
],
);
},
),
),
actions: <Widget>[
CustomMaterialButton(
isFilled: false,
label: I18nText('cancelButton'),
onPressed: () {
Navigator.of(context).pop();
},
),
CustomMaterialButton(
label: I18nText('okButton'),
onPressed: () {
setThemeMode(context, newTheme.value);
Navigator.of(context).pop();
},
),
],
),
);
}
}
final sUpdateTheme = SUpdateTheme();
class SUpdateThemeUI extends StatelessWidget {
const SUpdateThemeUI({super.key});
@ -63,10 +141,10 @@ class SUpdateThemeUI extends StatelessWidget {
return SettingsSection(
title: 'settingsView.appearanceSectionTitle',
children: <Widget>[
SwitchListTile(
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText(
'settingsView.darkThemeLabel',
'settingsView.themeModeLabel',
child: const Text(
'',
style: TextStyle(
@ -75,12 +153,11 @@ class SUpdateThemeUI extends StatelessWidget {
),
),
),
subtitle: I18nText('settingsView.darkThemeHint'),
value: SUpdateTheme().getDarkThemeStatus(),
onChanged: (value) => SUpdateTheme().setUseDarkTheme(
context,
value,
trailing: CustomMaterialButton(
label: sUpdateTheme.getThemeModeName(),
onPressed: () => { sUpdateTheme.showThemeDialog(context) },
),
onTap: () => { sUpdateTheme.showThemeDialog(context) },
),
FutureBuilder<int>(
future: _settingViewModel.getSdkVersion(),

View File

@ -71,12 +71,6 @@ class SettingsViewModel extends BaseViewModel {
actions: [
CustomMaterialButton(
isFilled: false,
label: I18nText('noButton'),
onPressed: () {
Navigator.of(context).pop();
},
),
CustomMaterialButton(
label: I18nText('yesButton'),
onPressed: () {
_managerAPI.setChangingToggleModified(true);
@ -84,6 +78,12 @@ class SettingsViewModel extends BaseViewModel {
Navigator.of(context).pop();
},
),
CustomMaterialButton(
label: I18nText('noButton'),
onPressed: () {
Navigator.of(context).pop();
},
),
],
),
);

View File

@ -222,22 +222,6 @@ class AppInfoView extends StatelessWidget {
subtitle: Text(app.packageName),
),
const SizedBox(height: 4),
ListTile(
contentPadding:
const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText(
'appInfoView.originalPackageNameLabel',
child: const Text(
'',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
),
),
),
subtitle: Text(app.originalPackageName),
),
const SizedBox(height: 4),
ListTile(
contentPadding:
const EdgeInsets.symmetric(horizontal: 20.0),

View File

@ -13,7 +13,6 @@ import 'package:revanced_manager/ui/views/home/home_viewmodel.dart';
import 'package:revanced_manager/ui/views/navigation/navigation_viewmodel.dart';
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
import 'package:revanced_manager/utils/string.dart';
import 'package:stacked/stacked.dart';
class AppInfoViewModel extends BaseViewModel {
@ -147,17 +146,7 @@ class AppInfoViewModel extends BaseViewModel {
}
String getAppliedPatchesString(List<String> appliedPatches) {
final List<String> names = appliedPatches
.map(
(p) => p
.replaceAll('-', ' ')
.split('-')
.join(' ')
.toTitleCase()
.replaceFirst('Microg', 'MicroG'),
)
.toList();
return '\u2022 ${names.join('\n\u2022 ')}';
return '\u2022 ${appliedPatches.join('\n\u2022 ')}';
}
void openApp(PatchedApplication app) {

View File

@ -79,8 +79,6 @@ class InstalledAppsCard extends StatelessWidget {
icon: app.icon,
name: app.name,
patchDate: app.patchDate,
changelog: app.changelog,
isUpdatableApp: false,
onPressed: () =>
locator<HomeViewModel>().navigateToAppInfo(app),
),

View File

@ -5,8 +5,8 @@ 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_enable_patches_selection.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';

View File

@ -75,8 +75,8 @@ class SocialMediaWidget extends StatelessWidget {
SocialMediaItem(
icon: FaIcon(FontAwesomeIcons.youtube),
title: Text('YouTube'),
subtitle: Text('youtube.com/revanced'),
url: 'https://youtube.com/revanced',
subtitle: Text('youtube.com/@revanced'),
url: 'https://youtube.com/@revanced',
),
],
),

View File

@ -1,6 +1,5 @@
import 'dart:typed_data';
import 'package:expandable/expandable.dart';
import 'package:flutter/material.dart';
import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
@ -13,151 +12,84 @@ class ApplicationItem extends StatefulWidget {
required this.icon,
required this.name,
required this.patchDate,
required this.changelog,
required this.isUpdatableApp,
required this.onPressed,
}) : super(key: key);
final Uint8List icon;
final String name;
final DateTime patchDate;
final List<String> changelog;
final bool isUpdatableApp;
final Function() onPressed;
@override
State<ApplicationItem> createState() => _ApplicationItemState();
}
class _ApplicationItemState extends State<ApplicationItem>
with TickerProviderStateMixin {
late AnimationController _animationController;
class _ApplicationItemState extends State<ApplicationItem> {
@override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 300),
);
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final ExpandableController expController = ExpandableController();
return Container(
margin: const EdgeInsets.only(bottom: 16.0),
child: CustomCard(
onTap: () {
expController.toggle();
_animationController.isCompleted
? _animationController.reverse()
: _animationController.forward();
},
child: ExpandablePanel(
controller: expController,
theme: const ExpandableThemeData(
inkWellBorderRadius: BorderRadius.all(Radius.circular(16)),
tapBodyToCollapse: false,
tapBodyToExpand: false,
tapHeaderToExpand: false,
hasIcon: false,
animationDuration: Duration(milliseconds: 450),
),
header: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Row(
children: [
SizedBox(
width: 40,
child: Image.memory(widget.icon, height: 40, width: 40),
),
const SizedBox(width: 19),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
widget.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
Text(
format(widget.patchDate),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
],
),
),
],
),
),
Row(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Row(
children: [
RotationTransition(
turns: Tween(begin: 0.0, end: 0.50)
.animate(_animationController),
child: const Padding(
padding: EdgeInsets.all(8.0),
child: Icon(Icons.arrow_drop_down),
),
SizedBox(
width: 40,
child: Image.memory(widget.icon, height: 40, width: 40),
),
const SizedBox(width: 8),
Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
CustomMaterialButton(
label: widget.isUpdatableApp
? I18nText('applicationItem.patchButton')
: I18nText('applicationItem.infoButton'),
onPressed: widget.onPressed,
),
],
const SizedBox(width: 19),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
widget.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
Text(
format(widget.patchDate),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
],
),
),
],
),
],
),
collapsed: const SizedBox(),
expanded: Padding(
padding: const EdgeInsets.only(
top: 16.0,
left: 4.0,
right: 4.0,
bottom: 4.0,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
I18nText(
'applicationItem.changelogLabel',
child: const Text(
'',
style: TextStyle(fontWeight: FontWeight.w700),
),
Row(
children: [
const SizedBox(width: 8),
Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
CustomMaterialButton(
label: I18nText('applicationItem.infoButton'),
onPressed: widget.onPressed,
),
],
),
const SizedBox(height: 4),
Text('\u2022 ${widget.changelog.join('\n\u2022 ')}'),
],
),
),
],
),
),
);

View File

@ -4,7 +4,7 @@ homepage: https://github.com/revanced/revanced-manager
publish_to: 'none'
version: 1.8.0+100800000
version: 1.11.0+101100000
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
synchronized: ^3.1.0
dev_dependencies:
json_serializable: ^6.6.1