Compare commits

...

51 Commits

Author SHA1 Message Date
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
24 changed files with 556 additions and 551 deletions

View File

@ -1,5 +1,5 @@
name: 🐞 Bug report name: 🐞 Bug report
description: Report a very clearly broken issue. description: Create a new bug report.
title: 'bug: <title>' title: 'bug: <title>'
labels: [bug] labels: [bug]
body: body:
@ -8,53 +8,20 @@ body:
value: | value: |
# ReVanced Manager bug report # 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). Please check for existing issues [here](https://github.com/revanced/revanced-manager/labels/bug) before creating a new one.
- type: dropdown
attributes:
label: Type
options:
- Error while running the manager
- Error at runtime
- Cosmetic
- Other
validations:
required: true
- type: textarea - type: textarea
attributes: attributes:
label: Bug description 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: validations:
required: true required: true
- type: textarea - type: textarea
attributes: attributes:
label: Steps to reproduce label: Version of ReVanced Manager and version & name of application you tried to patch
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.
validations: validations:
required: true required: true
- type: dropdown - type: dropdown
@ -64,57 +31,31 @@ body:
- Non-root - Non-root
- Root - Root
validations: validations:
required: true required: false
- type: textarea - type: textarea
attributes: attributes:
label: Patches selected. label: Device logs
description: Patches you selected for the app. description: Export logs in ReVanced Manager settings.
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.
render: shell render: shell
validations: validations:
required: true required: true
- type: textarea - type: textarea
attributes: attributes:
label: Installer logs (exported using Installer menu option) [unneeded if the issue is not during patching]. label: Patcher logs
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so there is no need for backticks. description: Export logs in "Patcher" screen.
render: shell render: shell
validations: validations:
required: false 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 - type: checkboxes
id: acknowledgments
attributes: attributes:
label: Acknowledgments 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: 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 required: true
- label: I have written a short but informative title. - label: I have chosen an appropriate title.
required: true 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 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 required: true

View File

@ -1,52 +1,42 @@
name: ⭐ Feature request name: ⭐ Feature request
description: Create a detailed feature request. description: Create a new feature request.
title: 'feat: <title>' title: 'feat: <title>'
labels: [feature-request] labels: [feature-request]
body: body:
- type: dropdown - type: markdown
attributes: attributes:
label: Type value: |
options: # ReVanced Manager feature request
- Functionality
- Cosmetic Please check for existing feature requests [here](https://github.com/revanced/revanced-manager/labels/bug) before creating a new one.
- Other
validations:
required: true
- type: textarea - type: textarea
attributes: attributes:
label: Issue label: Feature description
description: What is the current problem. Why does it require a feature request? description: Describe your feature in detail.
validations:
required: true
- type: textarea
attributes:
label: Feature
description: Describe your feature in detail. How does it solve the issue?
validations: validations:
required: true required: true
- type: textarea - type: textarea
attributes: attributes:
label: Motivation label: Motivation
description: Why should your feature should be considered? description: Explain why the lack of it is a problem.
validations: validations:
required: true required: true
- type: textarea - type: textarea
attributes: attributes:
label: Additional context label: Additional context
description: Add additional context here. description: In case there is something else you want to add.
validations: validations:
required: false required: false
- type: checkboxes - type: checkboxes
id: acknowledgements
attributes: attributes:
label: Acknowledgements 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: 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 required: true
- label: I have written a short but informative title. - label: I have chosen an appropriate title.
required: true 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 required: true
- label: The issue is related solely to the ReVanced Manager - label: The issue is solely related to the ReVanced Manager
required: true 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 runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
# Make sure the release step uses its own credentials: # Make sure the release step uses its own credentials:
# https://github.com/cycjimmy/semantic-release-action#private-packages # https://github.com/cycjimmy/semantic-release-action#private-packages

View File

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

1
.gitignore vendored
View File

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

View File

@ -3,29 +3,33 @@
The official ReVanced Manager based on Flutter. The official ReVanced Manager based on Flutter.
## 🔽 Download ## 🔽 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 ## 📝 Prerequisites
1. Android 8 or higher 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 ## 🔴 Issues
For suggestions and bug reports, open an issue [here](https://github.com/revanced/revanced-manager/issues/new/choose). 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 ## 🌐 Translation
[![Crowdin](https://badges.crowdin.net/revanced/localized.svg)](https://crowdin.com/project/revanced) [![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 ## 🛠️ Building Manager from source
1. Setup flutter environment for your [platform](https://docs.flutter.dev/get-started/install) 1. Setup flutter environment for your [platform](https://docs.flutter.dev/get-started/install)
2. Clone the repository locally 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 4. Open the project in terminal
5. Run `flutter pub get` 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) 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 { buildTypes {
release { release {
shrinkResources false
minifyEnabled false
resValue "string", "app_name", "ReVanced Manager" resValue "string", "app_name", "ReVanced Manager"
signingConfig signingConfigs.debug signingConfig signingConfigs.debug
ndk { ndk {
@ -83,10 +85,9 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
// ReVanced // ReVanced
implementation "app.revanced:revanced-patcher:11.0.4" implementation "app.revanced:revanced-patcher:14.2.2"
// Signing & aligning // Signing & aligning
implementation("org.bouncycastle:bcpkix-jdk15on:1.70") implementation("org.bouncycastle:bcpkix-jdk15on:1.70")
implementation("com.android.tools.build:apksig:7.2.2") implementation("com.android.tools.build:apksig:7.2.2")
} }

View File

@ -25,8 +25,7 @@
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:largeHeap="true" android:largeHeap="true"
android:requestLegacyExternalStorage="true" android:requestLegacyExternalStorage="true"
android:extractNativeLibs="true" android:extractNativeLibs="true">
android:enableOnBackInvokedCallback="true">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"

View File

@ -1,28 +1,31 @@
package app.revanced.manager.flutter package app.revanced.manager.flutter
import android.os.Build
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import androidx.annotation.NonNull
import app.revanced.manager.flutter.utils.Aapt import app.revanced.manager.flutter.utils.Aapt
import app.revanced.manager.flutter.utils.aligning.ZipAligner import app.revanced.manager.flutter.utils.aligning.ZipAligner
import app.revanced.manager.flutter.utils.signing.Signer import app.revanced.manager.flutter.utils.signing.Signer
import app.revanced.manager.flutter.utils.zip.ZipFile import app.revanced.manager.flutter.utils.zip.ZipFile
import app.revanced.manager.flutter.utils.zip.structures.ZipEntry import app.revanced.manager.flutter.utils.zip.structures.ZipEntry
import app.revanced.patcher.PatchBundleLoader
import app.revanced.patcher.Patcher import app.revanced.patcher.Patcher
import app.revanced.patcher.PatcherOptions import app.revanced.patcher.PatcherOptions
import app.revanced.patcher.extensions.PatchExtensions.compatiblePackages import app.revanced.patcher.extensions.PatchExtensions.compatiblePackages
import app.revanced.patcher.extensions.PatchExtensions.dependencies
import app.revanced.patcher.extensions.PatchExtensions.description
import app.revanced.patcher.extensions.PatchExtensions.include
import app.revanced.patcher.extensions.PatchExtensions.patchName import app.revanced.patcher.extensions.PatchExtensions.patchName
import app.revanced.patcher.logging.Logger import app.revanced.patcher.patch.PatchResult
import app.revanced.patcher.util.patch.PatchBundle
import dalvik.system.DexClassLoader
import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
import kotlinx.coroutines.cancel
import kotlinx.coroutines.runBlocking
import java.io.File import java.io.File
import java.io.PrintWriter
private const val PATCHER_CHANNEL = "app.revanced.manager.flutter/patcher" import java.io.StringWriter
private const val INSTALLER_CHANNEL = "app.revanced.manager.flutter/installer" import java.util.logging.LogRecord
import java.util.logging.Logger
class MainActivity : FlutterActivity() { class MainActivity : FlutterActivity() {
private val handler = Handler(Looper.getMainLooper()) private val handler = Handler(Looper.getMainLooper())
@ -30,10 +33,18 @@ class MainActivity : FlutterActivity() {
private var cancel: Boolean = false private var cancel: Boolean = false
private var stopResult: MethodChannel.Result? = null private var stopResult: MethodChannel.Result? = null
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(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 -> mainChannel.setMethodCallHandler { call, result ->
when (call.method) { when (call.method) {
"runPatcher" -> { "runPatcher" -> {
@ -73,14 +84,37 @@ class MainActivity : FlutterActivity() {
keyStoreFilePath, keyStoreFilePath,
keystorePassword keystorePassword
) )
} else { } else result.notImplemented()
result.notImplemented()
}
} }
"stopPatcher" -> { "stopPatcher" -> {
cancel = true cancel = true
stopResult = result stopResult = result
} }
"getPatches" -> {
val patchBundleFilePath = call.argument<String>("patchBundleFilePath")
if (patchBundleFilePath != null) {
val patches = PatchBundleLoader.Dex(
File(patchBundleFilePath)
).map { patch ->
val map = HashMap<String, Any>()
map["\"name\""] = "\"${patch.patchName.replace("\"","\\\"")}\""
map["\"description\""] = "\"${patch.description?.replace("\"","\\\"")}\""
map["\"excluded\""] = !patch.include
map["\"dependencies\""] = patch.dependencies?.map { "\"${it.java.patchName}\"" } ?: emptyList<Any>()
map["\"compatiblePackages\""] = patch.compatiblePackages?.map {
val map2 = HashMap<String, Any>()
map2["\"name\""] = "\"${it.name}\""
map2["\"versions\""] = it.versions.map { version -> "\"${version}\"" }
map2
} ?: emptyList<Any>()
map
}
result.success(patches)
} else result.notImplemented()
}
else -> result.notImplemented() else -> result.notImplemented()
} }
} }
@ -105,179 +139,141 @@ class MainActivity : FlutterActivity() {
val outFile = File(outFilePath) val outFile = File(outFilePath)
val integrations = File(integrationsPath) val integrations = File(integrationsPath)
val keyStoreFile = File(keyStoreFilePath) val keyStoreFile = File(keyStoreFilePath)
val cacheDir = File(cacheDirPath)
Thread { Thread {
try { fun updateProgress(progress: Double, header: String, log: String) {
handler.post { handler.post {
installerChannel.invokeMethod( installerChannel.invokeMethod(
"update", "update",
mapOf( mapOf(
"progress" to 0.1, "progress" to progress,
"header" to "", "header" to header,
"log" to "Copying original apk" "log" to log
) )
) )
} }
}
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) =
updateProgress(-1.0, "", record.message)
override fun flush() = Unit
override fun close() = flush()
}.let(::addHandler)
}
try {
updateProgress(0.0, "", "Copying APK")
if (cancel) { if (cancel) {
handler.post { stopResult!!.success(null) } postStop()
return@Thread return@Thread
} }
originalFile.copyTo(inputFile, true) 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) { if (cancel) {
handler.post { stopResult!!.success(null) } postStop()
return@Thread return@Thread
} }
val patcher = updateProgress(0.05, "Reading APK...", "Reading APK")
Patcher(
val patcher = Patcher(
PatcherOptions( PatcherOptions(
inputFile, inputFile,
cacheDirPath, cacheDir,
Aapt.binary(applicationContext).absolutePath, Aapt.binary(applicationContext).absolutePath,
cacheDirPath, cacheDir.path,
logger = ManagerLogger()
) )
) )
if (cancel) { if (cancel) {
handler.post { stopResult!!.success(null) } postStop()
return@Thread return@Thread
} }
handler.post { updateProgress(0.1, "Loading patches...", "Loading patches")
installerChannel.invokeMethod(
"update", val patches = PatchBundleLoader.Dex(
mapOf("progress" to 0.3, "header" to "", "log" to "") File(patchBundleFilePath),
) optimizedDexDirectory = cacheDir
} ).filter { patch ->
handler.post { val isCompatible = patch.compatiblePackages?.any {
installerChannel.invokeMethod( it.name == patcher.context.packageMetadata.packageName
"update", } ?: false
mapOf(
"progress" to 0.4, val compatibleOrUniversal =
"header" to "Merging integrations...", isCompatible || patch.compatiblePackages.isNullOrEmpty()
"log" to "Merging integrations"
) compatibleOrUniversal && selectedPatches.any { it == patch.patchName }
)
} }
if (cancel) { if (cancel) {
handler.post { stopResult!!.success(null) } postStop()
return@Thread return@Thread
} }
patcher.addIntegrations(listOf(integrations)) {} updateProgress(0.15, "Executing...", "")
// 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.apply {
acceptIntegrations(listOf(integrations))
acceptPatches(patches)
runBlocking {
apply(false).collect { patchResult: PatchResult ->
if (cancel) { if (cancel) {
handler.post { stopResult!!.success(null) } handler.post { stopResult!!.success(null) }
return@Thread this.cancel()
this@apply.close()
return@collect
} }
handler.post { val msg = patchResult.exception?.let {
installerChannel.invokeMethod( val writer = StringWriter()
"update", it.printStackTrace(PrintWriter(writer))
mapOf( "${patchResult.patchName} failed: $writer"
"progress" to 0.5, } ?: run {
"header" to "Applying patches...", "${patchResult.patchName} succeeded"
"log" to "" }
)
) updateProgress(progress, "", msg)
progress += progressStep
}
}
} }
if (cancel) { if (cancel) {
handler.post { stopResult!!.success(null) } postStop()
patcher.close()
return@Thread return@Thread
} }
val patches = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.CUPCAKE) { updateProgress(0.8, "Building...", "")
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) { val res = patcher.get()
handler.post { stopResult!!.success(null) } patcher.close()
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
)
)
}
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) }
return@Thread
}
val res = patcher.save()
ZipFile(patchedFile).use { file -> ZipFile(patchedFile).use { file ->
res.dexFiles.forEach { res.dexFiles.forEach {
if (cancel) { if (cancel) {
handler.post { stopResult!!.success(null) } postStop()
return@Thread return@Thread
} }
file.addEntryCompressData( file.addEntryCompressData(
@ -296,90 +292,35 @@ class MainActivity : FlutterActivity() {
ZipAligner::getEntryAlignment ZipAligner::getEntryAlignment
) )
} }
if (cancel) { if (cancel) {
handler.post { stopResult!!.success(null) } postStop()
return@Thread return@Thread
} }
handler.post {
installerChannel.invokeMethod( updateProgress(0.9, "Signing...", "Signing APK")
"update",
mapOf(
"progress" to 0.9,
"header" to "Signing apk...",
"log" to ""
)
)
}
try { try {
Signer("ReVanced", keystorePassword).signApk( Signer("ReVanced", keystorePassword)
patchedFile, .signApk(patchedFile, outFile, keyStoreFile)
outFile,
keyStoreFile
)
} catch (e: Exception) { } catch (e: Exception) {
//log to console print("Error signing APK: ${e.message}")
print("Error signing apk: ${e.message}")
e.printStackTrace() e.printStackTrace()
} }
handler.post { updateProgress(1.0, "Patched", "Patched")
installerChannel.invokeMethod(
"update",
mapOf(
"progress" to 1.0,
"header" to "Finished!",
"log" to "Finished!"
)
)
}
} catch (ex: Throwable) { } catch (ex: Throwable) {
if (!cancel) {
val stack = ex.stackTraceToString() val stack = ex.stackTraceToString()
handler.post { updateProgress(
installerChannel.invokeMethod( -100.0,
"update", "Aborted",
mapOf( "An error occurred:\n$stack"
"progress" to -100.0,
"header" to "Aborted...",
"log" to "An error occurred! Aborted\nError:\n$stack"
)
) )
} }
} }
handler.post { result.success(null) } handler.post { result.success(null) }
}.start() }.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 { buildscript {
ext.kotlin_version = '1.7.10' ext.kotlin_version = '1.9.0'
repositories { repositories {
google() google()
mavenCentral() mavenCentral()

View File

@ -178,8 +178,10 @@
"exportSectionTitle": "Import & export", "exportSectionTitle": "Import & export",
"logsSectionTitle": "Logs", "logsSectionTitle": "Logs",
"darkThemeLabel": "Dark mode", "themeModeLabel": "App theme",
"darkThemeHint": "Welcome to the dark side", "systemThemeLabel": "System",
"lightThemeLabel": "Light",
"darkThemeLabel": "Dark",
"dynamicThemeLabel": "Material You", "dynamicThemeLabel": "Material You",
"dynamicThemeHint": "Enjoy an experience closer to your device", "dynamicThemeHint": "Enjoy an experience closer to your device",

View File

@ -222,10 +222,8 @@ class GithubAPI {
final String downloadUrl = asset['browser_download_url']; final String downloadUrl = asset['browser_download_url'];
if (extension == '.apk') { if (extension == '.apk') {
_managerAPI.setIntegrationsDownloadURL(downloadUrl); _managerAPI.setIntegrationsDownloadURL(downloadUrl);
} else if (extension == '.json') {
_managerAPI.setPatchesDownloadURL(downloadUrl, false);
} else { } else {
_managerAPI.setPatchesDownloadURL(downloadUrl, true); _managerAPI.setPatchesDownloadURL(downloadUrl);
} }
return await DefaultCacheManager().getSingleFile( return await DefaultCacheManager().getSingleFile(
downloadUrl, downloadUrl,

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/patch.dart';
import 'package:revanced_manager/models/patched_application.dart'; import 'package:revanced_manager/models/patched_application.dart';
import 'package:revanced_manager/services/github_api.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/revanced_api.dart';
import 'package:revanced_manager/services/root_api.dart'; import 'package:revanced_manager/services/root_api.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.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 patcherRepo = 'revanced-patcher';
final String cliRepo = 'revanced-cli'; final String cliRepo = 'revanced-cli';
late SharedPreferences _prefs; late SharedPreferences _prefs;
List<Patch> patches = [];
bool isRooted = false; bool isRooted = false;
String storedPatchesFile = '/selected-patches.json'; String storedPatchesFile = '/selected-patches.json';
String keystoreFile = String keystoreFile =
@ -41,11 +43,11 @@ class ManagerAPI {
String? patchesVersion = ''; String? patchesVersion = '';
String? integrationsVersion = ''; String? integrationsVersion = '';
bool isDefaultPatchesRepo() { bool isDefaultPatchesRepo() {
return getPatchesRepo() == 'revanced/revanced-patches'; return getPatchesRepo().toLowerCase() == 'revanced/revanced-patches';
} }
bool isDefaultIntegrationsRepo() { bool isDefaultIntegrationsRepo() {
return getIntegrationsRepo() == 'revanced/revanced-integrations'; return getIntegrationsRepo().toLowerCase() == 'revanced/revanced-integrations';
} }
Future<void> initialize() async { Future<void> initialize() async {
@ -79,12 +81,12 @@ class ManagerAPI {
await _prefs.setString('repoUrl', url); await _prefs.setString('repoUrl', url);
} }
String getPatchesDownloadURL(bool bundle) { String getPatchesDownloadURL() {
return _prefs.getString('patchesDownloadURL-$bundle') ?? ''; return _prefs.getString('patchesDownloadURL') ?? '';
} }
Future<void> setPatchesDownloadURL(String value, bool bundle) async { Future<void> setPatchesDownloadURL(String value) async {
await _prefs.setString('patchesDownloadURL-$bundle', value); await _prefs.setString('patchesDownloadURL', value);
} }
String getPatchesRepo() { String getPatchesRepo() {
@ -111,17 +113,6 @@ class ManagerAPI {
} }
bool isPatchesChangeEnabled() { 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; return _prefs.getBool('patchesChangeEnabled') ?? false;
} }
@ -208,12 +199,12 @@ class ManagerAPI {
await _prefs.setBool('useDynamicTheme', value); await _prefs.setBool('useDynamicTheme', value);
} }
bool getUseDarkTheme() { int getThemeMode() {
return _prefs.getBool('useDarkTheme') ?? false; return _prefs.getInt('themeMode') ?? 2;
} }
Future<void> setUseDarkTheme(bool value) async { Future<void> setThemeMode(int value) async {
await _prefs.setBool('useDarkTheme', value); await _prefs.setInt('themeMode', value);
} }
bool areUniversalPatchesEnabled() { bool areUniversalPatchesEnabled() {
@ -311,28 +302,38 @@ class ManagerAPI {
} }
Future<List<Patch>> getPatches() async { Future<List<Patch>> getPatches() async {
if (patches.isNotEmpty) {
return patches;
}
final File? patchBundleFile = await downloadPatches();
if (patchBundleFile != null) {
try { try {
final String repoName = getPatchesRepo(); final patchesObject = await PatcherAPI.patcherChannel.invokeMethod(
final String currentVersion = await getCurrentPatchesVersion(); 'getPatches',
final String url = getPatchesDownloadURL(false); {
return await _githubAPI.getPatches( 'patchBundleFilePath': patchBundleFile.path,
repoName, },
currentVersion,
url,
); );
final List<Map<String, dynamic>> patchesMap = [];
patchesObject.forEach((patch) {
patchesMap.add(jsonDecode('$patch'));
});
patches = patchesMap.map((patch) => Patch.fromJson(patch)).toList();
return patches;
} on Exception catch (e) { } on Exception catch (e) {
if (kDebugMode) { if (kDebugMode) {
print(e); print(e);
} }
return [];
} }
} }
return List.empty();
}
Future<File?> downloadPatches() async { Future<File?> downloadPatches() async {
try { try {
final String repoName = getPatchesRepo(); final String repoName = getPatchesRepo();
final String currentVersion = await getCurrentPatchesVersion(); final String currentVersion = await getCurrentPatchesVersion();
final String url = getPatchesDownloadURL(true); final String url = getPatchesDownloadURL();
return await _githubAPI.getPatchesReleaseFile( return await _githubAPI.getPatchesReleaseFile(
'.jar', '.jar',
repoName, repoName,
@ -458,8 +459,7 @@ class ManagerAPI {
Future<void> setCurrentPatchesVersion(String version) async { Future<void> setCurrentPatchesVersion(String version) async {
await _prefs.setString('patchesVersion', version); await _prefs.setString('patchesVersion', version);
await setPatchesDownloadURL('', false); await setPatchesDownloadURL('');
await setPatchesDownloadURL('', true);
await downloadPatches(); await downloadPatches();
} }

View File

@ -25,12 +25,13 @@ class PatcherAPI {
late Directory _tmpDir; late Directory _tmpDir;
late File _keyStoreFile; late File _keyStoreFile;
List<Patch> _patches = []; List<Patch> _patches = [];
List<Patch> _universalPatches = [];
List<String> _compatiblePackages = [];
Map filteredPatches = <String, List<Patch>>{}; Map filteredPatches = <String, List<Patch>>{};
File? _outFile; File? _outFile;
Future<void> initialize() async { Future<void> initialize() async {
await _loadPatches(); await _loadPatches();
await _managerAPI.downloadPatches();
await _managerAPI.downloadIntegrations(); await _managerAPI.downloadIntegrations();
final Directory appCache = await getTemporaryDirectory(); final Directory appCache = await getTemporaryDirectory();
_dataDir = await getExternalStorageDirectory() ?? appCache; _dataDir = await getExternalStorageDirectory() ?? appCache;
@ -45,6 +46,24 @@ class PatcherAPI {
} }
} }
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 { Future<void> _loadPatches() async {
try { try {
if (_patches.isEmpty) { if (_patches.isEmpty) {
@ -56,6 +75,9 @@ class PatcherAPI {
} }
_patches = List.empty(); _patches = List.empty();
} }
_compatiblePackages = getCompatiblePackages();
_universalPatches = getUniversalPatches();
} }
Future<List<ApplicationWithIcon>> getFilteredInstalledApps( Future<List<ApplicationWithIcon>> getFilteredInstalledApps(
@ -63,31 +85,23 @@ class PatcherAPI {
) async { ) async {
final List<ApplicationWithIcon> filteredApps = []; final List<ApplicationWithIcon> filteredApps = [];
final bool allAppsIncluded = final bool allAppsIncluded =
_patches.any((patch) => patch.compatiblePackages.isEmpty) && _universalPatches.isNotEmpty &&
showUniversalPatches; showUniversalPatches;
if (allAppsIncluded) { if (allAppsIncluded) {
final allPackages = await DeviceApps.getInstalledApplications( final appList = await DeviceApps.getInstalledApplications(
includeAppIcons: true, includeAppIcons: true,
onlyAppsWithLaunchIntent: true, onlyAppsWithLaunchIntent: true,
); );
for (final pkg in allPackages) {
if (!filteredApps.any((app) => app.packageName == pkg.packageName)) { for(final app in appList) {
final appInfo = await DeviceApps.getApp( filteredApps.add(app as ApplicationWithIcon);
pkg.packageName,
true,
) as ApplicationWithIcon?;
if (appInfo != null) {
filteredApps.add(appInfo);
} }
} }
} for (final packageName in _compatiblePackages) {
}
for (final Patch patch in _patches) {
for (final Package package in patch.compatiblePackages) {
try { try {
if (!filteredApps.any((app) => app.packageName == package.name)) { if (!filteredApps.any((app) => app.packageName == packageName)) {
final ApplicationWithIcon? app = await DeviceApps.getApp( final ApplicationWithIcon? app = await DeviceApps.getApp(
package.name, packageName,
true, true,
) as ApplicationWithIcon?; ) as ApplicationWithIcon?;
if (app != null) { if (app != null) {
@ -100,11 +114,14 @@ class PatcherAPI {
} }
} }
} }
}
return filteredApps; return filteredApps;
} }
List<Patch> getFilteredPatches(String packageName) { List<Patch> getFilteredPatches(String packageName) {
if (!_compatiblePackages.contains(packageName)) {
return _universalPatches;
}
final List<Patch> patches = _patches final List<Patch> patches = _patches
.where( .where(
(patch) => (patch) =>

View File

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

View File

@ -90,13 +90,17 @@ class AppSelectorViewModel extends BaseViewModel {
await DeviceApps.getApp(packageName, true) as ApplicationWithIcon?; await DeviceApps.getApp(packageName, true) as ApplicationWithIcon?;
if (app != null) { if (app != null) {
if (await checkSplitApk(packageName) && !isRooted) { if (await checkSplitApk(packageName) && !isRooted) {
if (context.mounted) {
return showSelectFromStorageDialog(context); return showSelectFromStorageDialog(context);
}
} else if (!await checkSplitApk(packageName) || isRooted) { } else if (!await checkSplitApk(packageName) || isRooted) {
selectApp(app); selectApp(app);
if (context.mounted) {
Navigator.pop(context); Navigator.pop(context);
} }
} }
} }
}
Future showSelectFromStorageDialog(BuildContext context) async { Future showSelectFromStorageDialog(BuildContext context) async {
return showDialog( return showDialog(

View File

@ -21,11 +21,23 @@ class InstallerView extends StatelessWidget {
bottom: false, bottom: false,
child: Scaffold( child: Scaffold(
floatingActionButton: Visibility( floatingActionButton: Visibility(
visible: !model.isPatching, visible: !model.isPatching && !model.hasErrors,
child: FloatingActionButton.extended( child: FloatingActionButton.extended(
label: I18nText('installerView.installButton'), label: I18nText(
icon: const Icon(Icons.file_download_outlined), model.isInstalled
onPressed: () => model.installTypeDialog(context), ? '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, elevation: 0,
), ),
), ),

View File

@ -182,7 +182,8 @@ class InstallerViewModel extends BaseViewModel {
backgroundColor: Theme.of(context).colorScheme.secondaryContainer, backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
icon: const Icon(Icons.file_download_outlined), icon: const Icon(Icons.file_download_outlined),
contentPadding: const EdgeInsets.symmetric(vertical: 16), contentPadding: const EdgeInsets.symmetric(vertical: 16),
content: ValueListenableBuilder( content: SingleChildScrollView(
child: ValueListenableBuilder(
valueListenable: installType, valueListenable: installType,
builder: (context, value, child) { builder: (context, value, child) {
return Column( return Column(
@ -209,7 +210,7 @@ class InstallerViewModel extends BaseViewModel {
RadioListTile( RadioListTile(
title: I18nText('installerView.installNonRootType'), title: I18nText('installerView.installNonRootType'),
subtitle: I18nText('installerView.installRecommendedType'), subtitle: I18nText('installerView.installRecommendedType'),
contentPadding: const EdgeInsets.symmetric(horizontal: 10), contentPadding: const EdgeInsets.symmetric(horizontal: 16),
value: 0, value: 0,
groupValue: value, groupValue: value,
onChanged: (selected) { onChanged: (selected) {
@ -218,7 +219,7 @@ class InstallerViewModel extends BaseViewModel {
), ),
RadioListTile( RadioListTile(
title: I18nText('installerView.installRootType'), title: I18nText('installerView.installRootType'),
contentPadding: const EdgeInsets.symmetric(horizontal: 10), contentPadding: const EdgeInsets.symmetric(horizontal: 16),
value: 1, value: 1,
groupValue: value, groupValue: value,
onChanged: (selected) { onChanged: (selected) {
@ -229,6 +230,7 @@ class InstallerViewModel extends BaseViewModel {
); );
}, },
), ),
),
actions: [ actions: [
CustomMaterialButton( CustomMaterialButton(
label: I18nText('cancelButton'), label: I18nText('cancelButton'),

View File

@ -18,24 +18,20 @@ class NavigationViewModel extends IndexTrackingViewModel {
Future<void> initialize(BuildContext context) async { Future<void> initialize(BuildContext context) async {
locator<Toast>().initialize(context); locator<Toast>().initialize(context);
final SharedPreferences prefs = await SharedPreferences.getInstance(); final SharedPreferences prefs = await SharedPreferences.getInstance();
requestManageExternalStorage(); await requestManageExternalStorage();
if (prefs.getBool('permissionsRequested') == null) { if (prefs.getBool('permissionsRequested') == null) {
await Permission.storage.request(); await Permission.storage.request();
await Permission.manageExternalStorage.request();
await prefs.setBool('permissionsRequested', true); await prefs.setBool('permissionsRequested', true);
RootAPI().hasRootPermissions().then( await RootAPI().hasRootPermissions().then(
(value) => Permission.requestInstallPackages.request().then( (value) => Permission.requestInstallPackages.request().then(
(value) => Permission.ignoreBatteryOptimizations.request(), (value) => Permission.ignoreBatteryOptimizations.request(),
), ),
); );
} }
if (prefs.getBool('useDarkTheme') == null) { if (prefs.getInt('themeMode') == null) {
final bool isDark = await prefs.setInt('themeMode', 0);
MediaQuery.platformBrightnessOf(context) != Brightness.light;
await prefs.setBool('useDarkTheme', isDark);
await DynamicTheme.of(context)!.setTheme(isDark ? 1 : 0);
} }
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
SystemChrome.setSystemUIOverlayStyle( SystemChrome.setSystemUIOverlayStyle(

View File

@ -199,7 +199,8 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
supportedPackageVersions: supportedPackageVersions:
model.getSupportedVersions(patch), model.getSupportedVersions(patch),
isUnsupported: !isPatchSupported(patch), isUnsupported: !isPatchSupported(patch),
isChangeEnabled: _managerAPI.isPatchesChangeEnabled(), isChangeEnabled:
_managerAPI.isPatchesChangeEnabled(),
isNew: model.isPatchNew( isNew: model.isPatchNew(
patch, patch,
model.getAppInfo().packageName, model.getAppInfo().packageName,
@ -221,8 +222,23 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
vertical: 10.0, vertical: 10.0,
), ),
child: Container(
padding: const EdgeInsets.only(
top: 10.0,
bottom: 10.0,
left: 5.0,
),
child: I18nText( child: I18nText(
'patchesSelectorView.universalPatches', 'patchesSelectorView.universalPatches',
child: Text(
'',
style: TextStyle(
color: Theme.of(context)
.colorScheme
.primary,
),
),
),
), ),
), ),
...model.getQueriedPatches(_query).map((patch) { ...model.getQueriedPatches(_query).map((patch) {
@ -236,7 +252,8 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
supportedPackageVersions: supportedPackageVersions:
model.getSupportedVersions(patch), model.getSupportedVersions(patch),
isUnsupported: !isPatchSupported(patch), isUnsupported: !isPatchSupported(patch),
isChangeEnabled: _managerAPI.isPatchesChangeEnabled(), isChangeEnabled:
_managerAPI.isPatchesChangeEnabled(),
isNew: false, isNew: false,
isSelected: model.isSelected(patch), isSelected: model.isSelected(patch),
onChanged: (value) => model.selectPatch( onChanged: (value) => model.selectPatch(

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/services/manager_api.dart';
import 'package:revanced_manager/ui/views/settings/settings_viewmodel.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/settingsView/settings_section.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
final _settingViewModel = SettingsViewModel(); final _settingViewModel = SettingsViewModel();
@ -24,37 +25,114 @@ class SUpdateTheme extends BaseViewModel {
Future<void> setUseDynamicTheme(BuildContext context, bool value) async { Future<void> setUseDynamicTheme(BuildContext context, bool value) async {
await _managerAPI.setUseDynamicTheme(value); await _managerAPI.setUseDynamicTheme(value);
final int currentTheme = DynamicTheme.of(context)!.themeId; final int currentTheme = (DynamicTheme.of(context)!.themeId ~/ 2) * 2;
if (currentTheme.isEven) { await DynamicTheme.of(context)!.setTheme(currentTheme + (value ? 1 : 0));
await DynamicTheme.of(context)!.setTheme(value ? 2 : 0);
} else {
await DynamicTheme.of(context)!.setTheme(value ? 3 : 1);
}
notifyListeners(); notifyListeners();
} }
bool getDarkThemeStatus() { int getThemeMode() {
return _managerAPI.getUseDarkTheme(); return _managerAPI.getThemeMode();
} }
Future<void> setUseDarkTheme(BuildContext context, bool value) async { Future<void> setThemeMode(BuildContext context, int value) async {
await _managerAPI.setUseDarkTheme(value); await _managerAPI.setThemeMode(value);
final int currentTheme = DynamicTheme.of(context)!.themeId; final bool isDynamicTheme = DynamicTheme.of(context)!.themeId.isEven;
if (currentTheme < 2) { await DynamicTheme.of(context)!.setTheme(value * 2 + (isDynamicTheme ? 0 : 1));
await DynamicTheme.of(context)!.setTheme(value ? 1 : 0); final bool isLight = value != 2 && (value == 1 || DynamicTheme.of(context)!.theme.brightness == Brightness.light);
} else {
await DynamicTheme.of(context)!.setTheme(value ? 3 : 2);
}
SystemChrome.setSystemUIOverlayStyle( SystemChrome.setSystemUIOverlayStyle(
SystemUiOverlayStyle( SystemUiOverlayStyle(
systemNavigationBarIconBrightness: systemNavigationBarIconBrightness:
value ? Brightness.light : Brightness.dark, isLight ? Brightness.dark : Brightness.light,
), ),
); );
notifyListeners(); 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 { class SUpdateThemeUI extends StatelessWidget {
const SUpdateThemeUI({super.key}); const SUpdateThemeUI({super.key});
@ -63,10 +141,10 @@ class SUpdateThemeUI extends StatelessWidget {
return SettingsSection( return SettingsSection(
title: 'settingsView.appearanceSectionTitle', title: 'settingsView.appearanceSectionTitle',
children: <Widget>[ children: <Widget>[
SwitchListTile( ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0), contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText( title: I18nText(
'settingsView.darkThemeLabel', 'settingsView.themeModeLabel',
child: const Text( child: const Text(
'', '',
style: TextStyle( style: TextStyle(
@ -75,12 +153,11 @@ class SUpdateThemeUI extends StatelessWidget {
), ),
), ),
), ),
subtitle: I18nText('settingsView.darkThemeHint'), trailing: CustomMaterialButton(
value: SUpdateTheme().getDarkThemeStatus(), label: sUpdateTheme.getThemeModeName(),
onChanged: (value) => SUpdateTheme().setUseDarkTheme( onPressed: () => { sUpdateTheme.showThemeDialog(context) },
context,
value,
), ),
onTap: () => { sUpdateTheme.showThemeDialog(context) },
), ),
FutureBuilder<int>( FutureBuilder<int>(
future: _settingViewModel.getSdkVersion(), future: _settingViewModel.getSdkVersion(),

View File

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

View File

@ -4,7 +4,7 @@ homepage: https://github.com/revanced/revanced-manager
publish_to: 'none' publish_to: 'none'
version: 1.8.0+100800000 version: 1.10.1+101000001
environment: environment:
sdk: '>=3.0.0 <4.0.0' sdk: '>=3.0.0 <4.0.0'