mirror of
https://github.com/ReVanced/revanced-manager.git
synced 2025-05-18 23:16:50 +08:00
Compare commits
94 Commits
Author | SHA1 | Date | |
---|---|---|---|
b1c9aedac3 | |||
a80415be02 | |||
d9acd0d74b | |||
7ae09159ba | |||
a709abd80c | |||
cd07f39b69 | |||
f7c11d07a8 | |||
b07439d402 | |||
d8eadc2a2d | |||
3a88d4d3e6 | |||
012110f008 | |||
4de274bf62 | |||
76b89baee3 | |||
697ae92031 | |||
c87f92b346 | |||
6961bb7fd0 | |||
6e26130744 | |||
123a375a27 | |||
2b4b3ca0a5 | |||
c4a795418f | |||
91837ebade | |||
0492e910ea | |||
36c86e22b1 | |||
6bdc0c7bb2 | |||
1e8d8f749a | |||
2e8e3b0d1e | |||
15b8613d3c | |||
8ce266bc94 | |||
8661d72e45 | |||
62505f2543 | |||
37986c58ec | |||
2968d96fe9 | |||
e7c8d0e78c | |||
83cbb34a5b | |||
7559c7b67e | |||
02822f4b38 | |||
96736afb94 | |||
72ae132fcd | |||
2250e1bcab | |||
d9d5b746c3 | |||
f1ea306291 | |||
378d62395a | |||
99c92069b9 | |||
2a89ef797f | |||
5838550188 | |||
e0e01ae3ee | |||
0983ba8a0f | |||
0bfa776ce7 | |||
d2b09936d1 | |||
68e9f0f7c1 | |||
c3d345de80 | |||
385c0e246a | |||
5ead49a5b7 | |||
c0760b1347 | |||
e01b323aee | |||
6f4866ef63 | |||
1b6d72661c | |||
c59d4aea81 | |||
6260a80738 | |||
e75d3c8273 | |||
b7acb475e9 | |||
42b6bbff7c | |||
4b8542b35b | |||
9ad1d6cbfb | |||
4cdd9acd73 | |||
f4b0a695d6 | |||
b525ea1ba4 | |||
c1fc2c4766 | |||
5c733932c7 | |||
d1218616ec | |||
2bf6a03d56 | |||
b6ee63c1ea | |||
6d08efdcd7 | |||
a0a43a5651 | |||
3af2f5b032 | |||
8f54b226b4 | |||
9f64011b26 | |||
c5fc54e721 | |||
fc8a4fc5b6 | |||
6f9ab232ae | |||
8cb96f1e45 | |||
5733acb77a | |||
e49bcb2a69 | |||
42e41c399f | |||
166a3180d3 | |||
3bf4982f23 | |||
f4e1cccfac | |||
7911a8f49e | |||
64a96fc3ce | |||
8e2cfbddc5 | |||
45fae3f0fd | |||
e45a7824c1 | |||
5d72c48a76 | |||
d6169c6fa2 |
97
.github/ISSUE_TEMPLATE/bug-issue.yml
vendored
97
.github/ISSUE_TEMPLATE/bug-issue.yml
vendored
@ -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
|
||||||
|
40
.github/ISSUE_TEMPLATE/feature-issue.yml
vendored
40
.github/ISSUE_TEMPLATE/feature-issue.yml
vendored
@ -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
|
||||||
|
38
.github/workflows/analyze.yml
vendored
38
.github/workflows/analyze.yml
vendored
@ -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
|
|
2
.github/workflows/pr-build.yml
vendored
2
.github/workflows/pr-build.yml
vendored
@ -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
|
||||||
|
2
.github/workflows/release-build.yml
vendored
2
.github/workflows/release-build.yml
vendored
@ -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
1
.gitignore
vendored
@ -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/
|
||||||
|
22
README.md
22
README.md
@ -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
|
||||||
|
|
||||||
[](https://crowdin.com/project/revanced)
|
[](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`
|
||||||
|
@ -85,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:16.0.1"
|
||||||
|
|
||||||
// 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")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
@ -43,6 +42,10 @@
|
|||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name=".ExportSettingsActivity"
|
||||||
|
android:exported="true">
|
||||||
|
</activity>
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="flutterEmbedding"
|
android:name="flutterEmbedding"
|
||||||
android:value="2" />
|
android:value="2" />
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -1,28 +1,30 @@
|
|||||||
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.PatchSet
|
||||||
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.patch.PatchResult
|
||||||
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 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 org.json.JSONArray
|
||||||
|
import org.json.JSONObject
|
||||||
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.lang.Error
|
||||||
|
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,14 +32,23 @@ 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) {
|
private lateinit var patches: PatchSet
|
||||||
|
|
||||||
|
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" -> {
|
||||||
val patchBundleFilePath = call.argument<String>("patchBundleFilePath")
|
|
||||||
val originalFilePath = call.argument<String>("originalFilePath")
|
val originalFilePath = call.argument<String>("originalFilePath")
|
||||||
val inputFilePath = call.argument<String>("inputFilePath")
|
val inputFilePath = call.argument<String>("inputFilePath")
|
||||||
val patchedFilePath = call.argument<String>("patchedFilePath")
|
val patchedFilePath = call.argument<String>("patchedFilePath")
|
||||||
@ -48,7 +59,7 @@ class MainActivity : FlutterActivity() {
|
|||||||
val keyStoreFilePath = call.argument<String>("keyStoreFilePath")
|
val keyStoreFilePath = call.argument<String>("keyStoreFilePath")
|
||||||
val keystorePassword = call.argument<String>("keystorePassword")
|
val keystorePassword = call.argument<String>("keystorePassword")
|
||||||
|
|
||||||
if (patchBundleFilePath != null &&
|
if (
|
||||||
originalFilePath != null &&
|
originalFilePath != null &&
|
||||||
inputFilePath != null &&
|
inputFilePath != null &&
|
||||||
patchedFilePath != null &&
|
patchedFilePath != null &&
|
||||||
@ -62,7 +73,6 @@ class MainActivity : FlutterActivity() {
|
|||||||
cancel = false
|
cancel = false
|
||||||
runPatcher(
|
runPatcher(
|
||||||
result,
|
result,
|
||||||
patchBundleFilePath,
|
|
||||||
originalFilePath,
|
originalFilePath,
|
||||||
inputFilePath,
|
inputFilePath,
|
||||||
patchedFilePath,
|
patchedFilePath,
|
||||||
@ -73,9 +83,7 @@ class MainActivity : FlutterActivity() {
|
|||||||
keyStoreFilePath,
|
keyStoreFilePath,
|
||||||
keystorePassword
|
keystorePassword
|
||||||
)
|
)
|
||||||
} else {
|
} else result.notImplemented()
|
||||||
result.notImplemented()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
"stopPatcher" -> {
|
"stopPatcher" -> {
|
||||||
@ -83,6 +91,47 @@ class MainActivity : FlutterActivity() {
|
|||||||
stopResult = result
|
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()
|
else -> result.notImplemented()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -90,7 +139,6 @@ class MainActivity : FlutterActivity() {
|
|||||||
|
|
||||||
private fun runPatcher(
|
private fun runPatcher(
|
||||||
result: MethodChannel.Result,
|
result: MethodChannel.Result,
|
||||||
patchBundleFilePath: String,
|
|
||||||
originalFilePath: String,
|
originalFilePath: String,
|
||||||
inputFilePath: String,
|
inputFilePath: String,
|
||||||
patchedFilePath: String,
|
patchedFilePath: String,
|
||||||
@ -107,179 +155,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
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if(cancel) {
|
fun postStop() = handler.post { stopResult!!.success(null) }
|
||||||
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
|
return@Thread
|
||||||
}
|
}
|
||||||
|
|
||||||
originalFile.copyTo(inputFile, true)
|
originalFile.copyTo(inputFile, true)
|
||||||
|
|
||||||
handler.post {
|
if (cancel) {
|
||||||
installerChannel.invokeMethod(
|
postStop()
|
||||||
"update",
|
|
||||||
mapOf(
|
|
||||||
"progress" to 0.2,
|
|
||||||
"header" to "Reading APK...",
|
|
||||||
"log" to "Reading input APK"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if(cancel) {
|
|
||||||
handler.post { stopResult!!.success(null) }
|
|
||||||
return@Thread
|
return@Thread
|
||||||
}
|
}
|
||||||
|
|
||||||
val patcher =
|
updateProgress(0.05, "Reading APK...", "Reading APK")
|
||||||
Patcher(
|
|
||||||
PatcherOptions(
|
val patcher = Patcher(
|
||||||
inputFile,
|
PatcherOptions(
|
||||||
cacheDirPath,
|
inputFile,
|
||||||
Aapt.binary(applicationContext).absolutePath,
|
cacheDir,
|
||||||
cacheDirPath,
|
Aapt.binary(applicationContext).absolutePath,
|
||||||
logger = ManagerLogger()
|
cacheDir.path,
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
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 = patches.filter { patch ->
|
||||||
mapOf("progress" to 0.3, "header" to "", "log" to "")
|
val isCompatible = patch.compatiblePackages?.any {
|
||||||
)
|
it.name == patcher.context.packageMetadata.packageName
|
||||||
}
|
} ?: false
|
||||||
handler.post {
|
|
||||||
installerChannel.invokeMethod(
|
val compatibleOrUniversal =
|
||||||
"update",
|
isCompatible || patch.compatiblePackages.isNullOrEmpty()
|
||||||
mapOf(
|
|
||||||
"progress" to 0.4,
|
compatibleOrUniversal && selectedPatches.any { it == patch.name }
|
||||||
"header" to "Merging integrations...",
|
|
||||||
"log" to "Merging integrations"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if(cancel) {
|
if (cancel) {
|
||||||
handler.post { stopResult!!.success(null) }
|
postStop()
|
||||||
return@Thread
|
return@Thread
|
||||||
}
|
}
|
||||||
|
|
||||||
patcher.addIntegrations(listOf(integrations)) {}
|
updateProgress(0.15, "Executing...", "")
|
||||||
|
|
||||||
if(cancel) {
|
// Update the progress bar every time a patch is executed from 0.15 to 0.7
|
||||||
handler.post { stopResult!!.success(null) }
|
val totalPatchesCount = patches.size
|
||||||
return@Thread
|
val progressStep = 0.55 / totalPatchesCount
|
||||||
}
|
var progress = 0.15
|
||||||
|
|
||||||
handler.post {
|
patcher.apply {
|
||||||
installerChannel.invokeMethod(
|
acceptIntegrations(listOf(integrations))
|
||||||
"update",
|
acceptPatches(patches)
|
||||||
mapOf(
|
|
||||||
"progress" to 0.5,
|
|
||||||
"header" to "Applying patches...",
|
|
||||||
"log" to ""
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if(cancel) {
|
runBlocking {
|
||||||
handler.post { stopResult!!.success(null) }
|
apply(false).collect { patchResult: PatchResult ->
|
||||||
return@Thread
|
if (cancel) {
|
||||||
}
|
handler.post { stopResult!!.success(null) }
|
||||||
|
this.cancel()
|
||||||
|
this@apply.close()
|
||||||
|
return@collect
|
||||||
|
}
|
||||||
|
|
||||||
val patches = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.CUPCAKE) {
|
val msg = patchResult.exception?.let {
|
||||||
PatchBundle.Dex(
|
val writer = StringWriter()
|
||||||
patchBundleFilePath,
|
it.printStackTrace(PrintWriter(writer))
|
||||||
DexClassLoader(
|
"${patchResult.patch.name} failed: $writer"
|
||||||
patchBundleFilePath,
|
} ?: run {
|
||||||
cacheDirPath,
|
"${patchResult.patch.name} succeeded"
|
||||||
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) {
|
updateProgress(progress, "", msg)
|
||||||
handler.post { stopResult!!.success(null) }
|
progress += progressStep
|
||||||
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 {
|
if (cancel) {
|
||||||
installerChannel.invokeMethod(
|
postStop()
|
||||||
"update",
|
patcher.close()
|
||||||
mapOf(
|
|
||||||
"progress" to 0.7,
|
|
||||||
"header" to "Repacking APK...",
|
|
||||||
"log" to ""
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if(cancel) {
|
|
||||||
handler.post { stopResult!!.success(null) }
|
|
||||||
return@Thread
|
return@Thread
|
||||||
}
|
}
|
||||||
val res = patcher.save()
|
|
||||||
|
updateProgress(0.8, "Building...", "")
|
||||||
|
|
||||||
|
val res = patcher.get()
|
||||||
|
patcher.close()
|
||||||
|
|
||||||
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(
|
||||||
@ -298,90 +308,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) {
|
||||||
val stack = ex.stackTraceToString()
|
if (!cancel) {
|
||||||
handler.post {
|
val stack = ex.stackTraceToString()
|
||||||
installerChannel.invokeMethod(
|
updateProgress(
|
||||||
"update",
|
-100.0,
|
||||||
mapOf(
|
"Failed",
|
||||||
"progress" to -100.0,
|
"An error occurred:\n$stack"
|
||||||
"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 */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
buildscript {
|
buildscript {
|
||||||
ext.kotlin_version = '1.7.10'
|
ext.kotlin_version = '1.9.0'
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
@ -23,13 +23,13 @@
|
|||||||
"widgetTitle": "Dashboard",
|
"widgetTitle": "Dashboard",
|
||||||
|
|
||||||
"updatesSubtitle": "Updates",
|
"updatesSubtitle": "Updates",
|
||||||
"patchedSubtitle": "Patched applications",
|
"patchedSubtitle": "Patched apps",
|
||||||
|
|
||||||
"noUpdates": "No updates available",
|
"noUpdates": "No updates available",
|
||||||
|
|
||||||
"WIP": "Work in progress...",
|
"WIP": "Work in progress...",
|
||||||
|
|
||||||
"noInstallations": "No patched applications installed",
|
"noInstallations": "No patched apps installed",
|
||||||
"installUpdate": "Continue to install the update?",
|
"installUpdate": "Continue to install the update?",
|
||||||
|
|
||||||
"updateDialogTitle": "Update Manager",
|
"updateDialogTitle": "Update Manager",
|
||||||
@ -56,9 +56,7 @@
|
|||||||
"updatesDisabled": "Updating a patched app is currently disabled. Repatch the app again."
|
"updatesDisabled": "Updating a patched app is currently disabled. Repatch the app again."
|
||||||
},
|
},
|
||||||
"applicationItem": {
|
"applicationItem": {
|
||||||
"patchButton": "Patch",
|
"infoButton": "Info"
|
||||||
"infoButton": "Info",
|
|
||||||
"changelogLabel": "Changelog"
|
|
||||||
},
|
},
|
||||||
"latestCommitCard": {
|
"latestCommitCard": {
|
||||||
"loadingLabel": "Loading...",
|
"loadingLabel": "Loading...",
|
||||||
@ -71,9 +69,8 @@
|
|||||||
"widgetTitle": "Patcher",
|
"widgetTitle": "Patcher",
|
||||||
"patchButton": "Patch",
|
"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?",
|
"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?"
|
"removedPatchesWarningDialogText": "The following patches have been removed since the last time you used them.\n\n{patches}\n\nProceed anyways?"
|
||||||
},
|
},
|
||||||
"appSelectorCard": {
|
"appSelectorCard": {
|
||||||
@ -148,9 +145,8 @@
|
|||||||
"installTypeDescription": "Select the installation type to proceed with.",
|
"installTypeDescription": "Select the installation type to proceed with.",
|
||||||
|
|
||||||
"installButton": "Install",
|
"installButton": "Install",
|
||||||
"installRootType": "Root",
|
"installRootType": "Mount",
|
||||||
"installNonRootType": "Non-root",
|
"installNonRootType": "Normal",
|
||||||
"installRecommendedType": "Recommended",
|
|
||||||
|
|
||||||
"pressBackAgain": "Press back again to cancel",
|
"pressBackAgain": "Press back again to cancel",
|
||||||
"openButton": "Open",
|
"openButton": "Open",
|
||||||
@ -162,10 +158,6 @@
|
|||||||
"exportApkButtonTooltip": "Export patched APK",
|
"exportApkButtonTooltip": "Export patched APK",
|
||||||
"exportLogButtonTooltip": "Export log",
|
"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..."
|
"noExit": "Installer is still running, cannot exit..."
|
||||||
},
|
},
|
||||||
"settingsView": {
|
"settingsView": {
|
||||||
@ -178,8 +170,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",
|
||||||
@ -283,7 +277,6 @@
|
|||||||
"rootDialogText": "App was installed with superuser permissions, but currently ReVanced Manager has no permissions.\nPlease grant superuser permissions first.",
|
"rootDialogText": "App was installed with superuser permissions, but currently ReVanced Manager has no permissions.\nPlease grant superuser permissions first.",
|
||||||
|
|
||||||
"packageNameLabel": "Package name",
|
"packageNameLabel": "Package name",
|
||||||
"originalPackageNameLabel": "Original package name",
|
|
||||||
"installTypeLabel": "Installation type",
|
"installTypeLabel": "Installation type",
|
||||||
"rootTypeLabel": "Root",
|
"rootTypeLabel": "Root",
|
||||||
"nonRootTypeLabel": "Non-root",
|
"nonRootTypeLabel": "Non-root",
|
||||||
|
@ -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)
|
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
|
```properties
|
||||||
gpr.user = YourUsername
|
gpr.user = YourUsername
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import 'package:json_annotation/json_annotation.dart';
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
import 'package:revanced_manager/utils/string.dart';
|
|
||||||
|
|
||||||
part 'patch.g.dart';
|
part 'patch.g.dart';
|
||||||
|
|
||||||
@ -9,26 +8,19 @@ class Patch {
|
|||||||
required this.name,
|
required this.name,
|
||||||
required this.description,
|
required this.description,
|
||||||
required this.excluded,
|
required this.excluded,
|
||||||
required this.dependencies,
|
|
||||||
required this.compatiblePackages,
|
required this.compatiblePackages,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory Patch.fromJson(Map<String, dynamic> json) => _$PatchFromJson(json);
|
factory Patch.fromJson(Map<String, dynamic> json) => _$PatchFromJson(json);
|
||||||
final String name;
|
final String name;
|
||||||
final String description;
|
final String? description;
|
||||||
final bool excluded;
|
final bool excluded;
|
||||||
final List<String> dependencies;
|
|
||||||
final List<Package> compatiblePackages;
|
final List<Package> compatiblePackages;
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => _$PatchToJson(this);
|
Map<String, dynamic> toJson() => _$PatchToJson(this);
|
||||||
|
|
||||||
String getSimpleName() {
|
String getSimpleName() {
|
||||||
return name
|
return name;
|
||||||
.replaceAll('-', ' ')
|
|
||||||
.split('-')
|
|
||||||
.join(' ')
|
|
||||||
.toTitleCase()
|
|
||||||
.replaceFirst('Microg', 'MicroG');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,23 +9,19 @@ class PatchedApplication {
|
|||||||
PatchedApplication({
|
PatchedApplication({
|
||||||
required this.name,
|
required this.name,
|
||||||
required this.packageName,
|
required this.packageName,
|
||||||
required this.originalPackageName,
|
|
||||||
required this.version,
|
required this.version,
|
||||||
required this.apkFilePath,
|
required this.apkFilePath,
|
||||||
required this.icon,
|
required this.icon,
|
||||||
required this.patchDate,
|
required this.patchDate,
|
||||||
this.isRooted = false,
|
this.isRooted = false,
|
||||||
this.isFromStorage = false,
|
this.isFromStorage = false,
|
||||||
this.hasUpdates = false,
|
|
||||||
this.appliedPatches = const [],
|
this.appliedPatches = const [],
|
||||||
this.changelog = const [],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
factory PatchedApplication.fromJson(Map<String, dynamic> json) =>
|
factory PatchedApplication.fromJson(Map<String, dynamic> json) =>
|
||||||
_$PatchedApplicationFromJson(json);
|
_$PatchedApplicationFromJson(json);
|
||||||
String name;
|
String name;
|
||||||
String packageName;
|
String packageName;
|
||||||
String originalPackageName;
|
|
||||||
String version;
|
String version;
|
||||||
final String apkFilePath;
|
final String apkFilePath;
|
||||||
@JsonKey(
|
@JsonKey(
|
||||||
@ -36,9 +32,7 @@ class PatchedApplication {
|
|||||||
DateTime patchDate;
|
DateTime patchDate;
|
||||||
bool isRooted;
|
bool isRooted;
|
||||||
bool isFromStorage;
|
bool isFromStorage;
|
||||||
bool hasUpdates;
|
|
||||||
List<String> appliedPatches;
|
List<String> appliedPatches;
|
||||||
List<String> changelog;
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => _$PatchedApplicationToJson(this);
|
Map<String, dynamic> toJson() => _$PatchedApplicationToJson(this);
|
||||||
|
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import 'dart:convert';
|
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:dio/dio.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:flutter_cache_manager/flutter_cache_manager.dart';
|
||||||
import 'package:injectable/injectable.dart';
|
import 'package:injectable/injectable.dart';
|
||||||
import 'package:revanced_manager/app/app.locator.dart';
|
import 'package:revanced_manager/app/app.locator.dart';
|
||||||
import 'package:revanced_manager/models/patch.dart';
|
|
||||||
import 'package:revanced_manager/services/manager_api.dart';
|
import 'package:revanced_manager/services/manager_api.dart';
|
||||||
|
|
||||||
@lazySingleton
|
@lazySingleton
|
||||||
@ -21,17 +19,6 @@ class GithubAPI {
|
|||||||
priority: CachePriority.high,
|
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 {
|
Future<void> initialize(String repoUrl) async {
|
||||||
try {
|
try {
|
||||||
_dio = Dio(
|
_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(
|
Future<File?> getLatestReleaseFile(
|
||||||
String extension,
|
String extension,
|
||||||
String repoName,
|
String repoName,
|
||||||
@ -222,10 +177,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,
|
||||||
@ -239,30 +192,4 @@ class GithubAPI {
|
|||||||
}
|
}
|
||||||
return null;
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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 =
|
||||||
@ -40,12 +42,14 @@ class ManagerAPI {
|
|||||||
String defaultManagerRepo = 'revanced/revanced-manager';
|
String defaultManagerRepo = 'revanced/revanced-manager';
|
||||||
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 +83,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 +115,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 +201,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 +304,46 @@ class ManagerAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<List<Patch>> getPatches() async {
|
Future<List<Patch>> getPatches() async {
|
||||||
try {
|
if (patches.isNotEmpty) {
|
||||||
final String repoName = getPatchesRepo();
|
return patches;
|
||||||
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 [];
|
|
||||||
}
|
}
|
||||||
|
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 {
|
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 +469,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();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -495,62 +505,33 @@ class ManagerAPI {
|
|||||||
return toRemove;
|
return toRemove;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<PatchedApplication>> getUnsavedApps(
|
Future<List<PatchedApplication>> getMountedApps() async {
|
||||||
List<PatchedApplication> patchedApps,
|
final List<PatchedApplication> mountedApps = [];
|
||||||
) async {
|
|
||||||
final List<PatchedApplication> unsavedApps = [];
|
|
||||||
final bool hasRootPermissions = await _rootAPI.hasRootPermissions();
|
final bool hasRootPermissions = await _rootAPI.hasRootPermissions();
|
||||||
if (hasRootPermissions) {
|
if (hasRootPermissions) {
|
||||||
final List<String> installedApps = await _rootAPI.getInstalledApps();
|
final List<String> installedApps = await _rootAPI.getInstalledApps();
|
||||||
for (final String packageName in installedApps) {
|
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(
|
final ApplicationWithIcon? application = await DeviceApps.getApp(
|
||||||
app.packageName,
|
packageName,
|
||||||
true,
|
true,
|
||||||
) as ApplicationWithIcon?;
|
) as ApplicationWithIcon?;
|
||||||
if (application != null) {
|
if (application != null) {
|
||||||
unsavedApps.add(
|
mountedApps.add(
|
||||||
PatchedApplication(
|
PatchedApplication(
|
||||||
name: application.appName,
|
name: application.appName,
|
||||||
packageName: application.packageName,
|
packageName: application.packageName,
|
||||||
originalPackageName: application.packageName,
|
|
||||||
version: application.versionName!,
|
version: application.versionName!,
|
||||||
apkFilePath: application.apkFilePath,
|
apkFilePath: application.apkFilePath,
|
||||||
icon: application.icon,
|
icon: application.icon,
|
||||||
patchDate: DateTime.now(),
|
patchDate: DateTime.now(),
|
||||||
|
isRooted: true,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return unsavedApps;
|
|
||||||
|
return mountedApps;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> showPatchesChangeWarningDialog(BuildContext context) {
|
Future<void> showPatchesChangeWarningDialog(BuildContext context) {
|
||||||
@ -612,34 +593,20 @@ class ManagerAPI {
|
|||||||
|
|
||||||
Future<void> reAssessSavedApps() async {
|
Future<void> reAssessSavedApps() async {
|
||||||
final List<PatchedApplication> patchedApps = getPatchedApps();
|
final List<PatchedApplication> patchedApps = getPatchedApps();
|
||||||
final List<PatchedApplication> unsavedApps =
|
|
||||||
await getUnsavedApps(patchedApps);
|
// Remove apps that are not installed anymore.
|
||||||
patchedApps.addAll(unsavedApps);
|
|
||||||
final List<PatchedApplication> toRemove =
|
final List<PatchedApplication> toRemove =
|
||||||
await getAppsToRemove(patchedApps);
|
await getAppsToRemove(patchedApps);
|
||||||
patchedApps.removeWhere((a) => toRemove.contains(a));
|
patchedApps.removeWhere((a) => toRemove.contains(a));
|
||||||
for (final PatchedApplication app in patchedApps) {
|
|
||||||
app.hasUpdates =
|
// Determine all apps that are installed by mounting.
|
||||||
await hasAppUpdates(app.originalPackageName, app.patchDate);
|
final List<PatchedApplication> mountedApps = await getMountedApps();
|
||||||
app.changelog =
|
mountedApps.removeWhere(
|
||||||
await getAppChangelog(app.originalPackageName, app.patchDate);
|
(app) => patchedApps
|
||||||
if (!app.hasUpdates) {
|
.any((patchedApp) => patchedApp.packageName == app.packageName),
|
||||||
final String? currentInstalledVersion =
|
);
|
||||||
(await DeviceApps.getApp(app.packageName))?.versionName;
|
patchedApps.addAll(mountedApps);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await setPatchedApps(patchedApps);
|
await setPatchedApps(patchedApps);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -656,37 +623,6 @@ class ManagerAPI {
|
|||||||
return !existsNonRoot;
|
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 {
|
Future<bool> isSplitApk(PatchedApplication patchedApp) async {
|
||||||
Application? app;
|
Application? app;
|
||||||
if (patchedApp.isFromStorage) {
|
if (patchedApp.isFromStorage) {
|
||||||
@ -754,6 +690,8 @@ class ManagerAPI {
|
|||||||
|
|
||||||
Future<void> resetLastSelectedPatches() async {
|
Future<void> resetLastSelectedPatches() async {
|
||||||
final File selectedPatchesFile = File(storedPatchesFile);
|
final File selectedPatchesFile = File(storedPatchesFile);
|
||||||
selectedPatchesFile.deleteSync();
|
if (selectedPatchesFile.existsSync()) {
|
||||||
|
selectedPatchesFile.deleteSync();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,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 {
|
try {
|
||||||
if (_patches.isEmpty) {
|
if (_patches.isEmpty) {
|
||||||
_patches = await _managerAPI.getPatches();
|
_patches = await _managerAPI.getPatches();
|
||||||
@ -56,6 +73,9 @@ class PatcherAPI {
|
|||||||
}
|
}
|
||||||
_patches = List.empty();
|
_patches = List.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_compatiblePackages = getCompatiblePackages();
|
||||||
|
_universalPatches = getUniversalPatches();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<ApplicationWithIcon>> getFilteredInstalledApps(
|
Future<List<ApplicationWithIcon>> getFilteredInstalledApps(
|
||||||
@ -63,48 +83,42 @@ 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 Patch patch in _patches) {
|
for (final packageName in _compatiblePackages) {
|
||||||
for (final Package package in patch.compatiblePackages) {
|
try {
|
||||||
try {
|
if (!filteredApps.any((app) => app.packageName == packageName)) {
|
||||||
if (!filteredApps.any((app) => app.packageName == package.name)) {
|
final ApplicationWithIcon? app = await DeviceApps.getApp(
|
||||||
final ApplicationWithIcon? app = await DeviceApps.getApp(
|
packageName,
|
||||||
package.name,
|
true,
|
||||||
true,
|
) as ApplicationWithIcon?;
|
||||||
) as ApplicationWithIcon?;
|
if (app != null) {
|
||||||
if (app != null) {
|
filteredApps.add(app);
|
||||||
filteredApps.add(app);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} on Exception catch (e) {
|
|
||||||
if (kDebugMode) {
|
|
||||||
print(e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} on Exception catch (e) {
|
||||||
|
if (kDebugMode) {
|
||||||
|
print(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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) =>
|
||||||
@ -132,55 +146,20 @@ class PatcherAPI {
|
|||||||
.toList();
|
.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(
|
Future<void> runPatcher(
|
||||||
String packageName,
|
String packageName,
|
||||||
String apkFilePath,
|
String apkFilePath,
|
||||||
List<Patch> selectedPatches,
|
List<Patch> selectedPatches,
|
||||||
) async {
|
) 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();
|
final File? integrationsFile = await _managerAPI.downloadIntegrations();
|
||||||
if (patchBundleFile != null) {
|
|
||||||
|
if (integrationsFile != null) {
|
||||||
_dataDir.createSync();
|
_dataDir.createSync();
|
||||||
_tmpDir.createSync();
|
_tmpDir.createSync();
|
||||||
final Directory workDir = _tmpDir.createTempSync('tmp-');
|
final Directory workDir = _tmpDir.createTempSync('tmp-');
|
||||||
final File inputFile = File('${workDir.path}/base.apk');
|
final File inputFile = File('${workDir.path}/base.apk');
|
||||||
final File patchedFile = File('${workDir.path}/patched.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');
|
final Directory cacheDir = Directory('${workDir.path}/cache');
|
||||||
cacheDir.createSync();
|
cacheDir.createSync();
|
||||||
final String originalFilePath = apkFilePath;
|
final String originalFilePath = apkFilePath;
|
||||||
@ -188,12 +167,11 @@ class PatcherAPI {
|
|||||||
await patcherChannel.invokeMethod(
|
await patcherChannel.invokeMethod(
|
||||||
'runPatcher',
|
'runPatcher',
|
||||||
{
|
{
|
||||||
'patchBundleFilePath': patchBundleFile.path,
|
|
||||||
'originalFilePath': originalFilePath,
|
'originalFilePath': originalFilePath,
|
||||||
'inputFilePath': inputFile.path,
|
'inputFilePath': inputFile.path,
|
||||||
'patchedFilePath': patchedFile.path,
|
'patchedFilePath': patchedFile.path,
|
||||||
'outFilePath': _outFile!.path,
|
'outFilePath': outFile!.path,
|
||||||
'integrationsPath': integrationsFile!.path,
|
'integrationsPath': integrationsFile.path,
|
||||||
'selectedPatches': selectedPatches.map((p) => p.name).toList(),
|
'selectedPatches': selectedPatches.map((p) => p.name).toList(),
|
||||||
'cacheDirPath': cacheDir.path,
|
'cacheDirPath': cacheDir.path,
|
||||||
'keyStoreFilePath': _keyStoreFile.path,
|
'keyStoreFilePath': _keyStoreFile.path,
|
||||||
@ -219,7 +197,7 @@ class PatcherAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> installPatchedFile(PatchedApplication patchedApp) async {
|
Future<bool> installPatchedFile(PatchedApplication patchedApp) async {
|
||||||
if (_outFile != null) {
|
if (outFile != null) {
|
||||||
try {
|
try {
|
||||||
if (patchedApp.isRooted) {
|
if (patchedApp.isRooted) {
|
||||||
final bool hasRootPermissions = await _rootAPI.hasRootPermissions();
|
final bool hasRootPermissions = await _rootAPI.hasRootPermissions();
|
||||||
@ -227,11 +205,11 @@ class PatcherAPI {
|
|||||||
return _rootAPI.installApp(
|
return _rootAPI.installApp(
|
||||||
patchedApp.packageName,
|
patchedApp.packageName,
|
||||||
patchedApp.apkFilePath,
|
patchedApp.apkFilePath,
|
||||||
_outFile!.path,
|
outFile!.path,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
final install = await InstallPlugin.installApk(_outFile!.path);
|
final install = await InstallPlugin.installApk(outFile!.path);
|
||||||
return install['isSuccess'];
|
return install['isSuccess'];
|
||||||
}
|
}
|
||||||
} on Exception catch (e) {
|
} on Exception catch (e) {
|
||||||
@ -246,11 +224,11 @@ class PatcherAPI {
|
|||||||
|
|
||||||
void exportPatchedFile(String appName, String version) {
|
void exportPatchedFile(String appName, String version) {
|
||||||
try {
|
try {
|
||||||
if (_outFile != null) {
|
if (outFile != null) {
|
||||||
final String newName = _getFileName(appName, version);
|
final String newName = _getFileName(appName, version);
|
||||||
CRFileSaver.saveFileWithDialog(
|
CRFileSaver.saveFileWithDialog(
|
||||||
SaveFileDialogParams(
|
SaveFileDialogParams(
|
||||||
sourceFilePath: _outFile!.path,
|
sourceFilePath: outFile!.path,
|
||||||
destinationFileName: newName,
|
destinationFileName: newName,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -264,12 +242,12 @@ class PatcherAPI {
|
|||||||
|
|
||||||
void sharePatchedFile(String appName, String version) {
|
void sharePatchedFile(String appName, String version) {
|
||||||
try {
|
try {
|
||||||
if (_outFile != null) {
|
if (outFile != null) {
|
||||||
final String newName = _getFileName(appName, version);
|
final String newName = _getFileName(appName, version);
|
||||||
final int lastSeparator = _outFile!.path.lastIndexOf('/');
|
final int lastSeparator = outFile!.path.lastIndexOf('/');
|
||||||
final String newPath =
|
final String newPath =
|
||||||
_outFile!.path.substring(0, lastSeparator + 1) + newName;
|
outFile!.path.substring(0, lastSeparator + 1) + newName;
|
||||||
final File shareFile = _outFile!.copySync(newPath);
|
final File shareFile = outFile!.copySync(newPath);
|
||||||
ShareExtend.share(shareFile.path, 'file');
|
ShareExtend.share(shareFile.path, 'file');
|
||||||
}
|
}
|
||||||
} on Exception catch (e) {
|
} on Exception catch (e) {
|
||||||
|
@ -7,12 +7,15 @@ import 'package:dio_cache_interceptor/dio_cache_interceptor.dart';
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||||
import 'package:injectable/injectable.dart';
|
import 'package:injectable/injectable.dart';
|
||||||
|
import 'package:synchronized/synchronized.dart';
|
||||||
import 'package:timeago/timeago.dart';
|
import 'package:timeago/timeago.dart';
|
||||||
|
|
||||||
@lazySingleton
|
@lazySingleton
|
||||||
class RevancedAPI {
|
class RevancedAPI {
|
||||||
late Dio _dio = Dio();
|
late Dio _dio = Dio();
|
||||||
|
|
||||||
|
final Lock getToolsLock = Lock();
|
||||||
|
|
||||||
final _cacheOptions = CacheOptions(
|
final _cacheOptions = CacheOptions(
|
||||||
store: MemCacheStore(),
|
store: MemCacheStore(),
|
||||||
maxStale: const Duration(days: 1),
|
maxStale: const Duration(days: 1),
|
||||||
@ -66,21 +69,23 @@ class RevancedAPI {
|
|||||||
Future<Map<String, dynamic>?> _getLatestRelease(
|
Future<Map<String, dynamic>?> _getLatestRelease(
|
||||||
String extension,
|
String extension,
|
||||||
String repoName,
|
String repoName,
|
||||||
) async {
|
) {
|
||||||
try {
|
return getToolsLock.synchronized(() async {
|
||||||
final response = await _dio.get('/tools');
|
try {
|
||||||
final List<dynamic> tools = response.data['tools'];
|
final response = await _dio.get('/tools');
|
||||||
return tools.firstWhereOrNull(
|
final List<dynamic> tools = response.data['tools'];
|
||||||
(t) =>
|
return tools.firstWhereOrNull(
|
||||||
t['repository'] == repoName &&
|
(t) =>
|
||||||
(t['name'] as String).endsWith(extension),
|
t['repository'] == repoName &&
|
||||||
);
|
(t['name'] as String).endsWith(extension),
|
||||||
} on Exception catch (e) {
|
);
|
||||||
if (kDebugMode) {
|
} on Exception catch (e) {
|
||||||
print(e);
|
if (kDebugMode) {
|
||||||
|
print(e);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
return null;
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String?> getLatestReleaseVersion(
|
Future<String?> getLatestReleaseVersion(
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -54,7 +54,7 @@ class _AppSelectorViewState extends State<AppSelectorView> {
|
|||||||
onPressed: () => Navigator.of(context).pop(),
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
),
|
),
|
||||||
bottom: PreferredSize(
|
bottom: PreferredSize(
|
||||||
preferredSize: const Size.fromHeight(64.0),
|
preferredSize: const Size.fromHeight(66.0),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
vertical: 8.0,
|
vertical: 8.0,
|
||||||
|
@ -73,7 +73,6 @@ class AppSelectorViewModel extends BaseViewModel {
|
|||||||
locator<PatcherViewModel>().selectedApp = PatchedApplication(
|
locator<PatcherViewModel>().selectedApp = PatchedApplication(
|
||||||
name: application.appName,
|
name: application.appName,
|
||||||
packageName: application.packageName,
|
packageName: application.packageName,
|
||||||
originalPackageName: application.packageName,
|
|
||||||
version: application.versionName!,
|
version: application.versionName!,
|
||||||
apkFilePath: application.apkFilePath,
|
apkFilePath: application.apkFilePath,
|
||||||
icon: application.icon,
|
icon: application.icon,
|
||||||
@ -90,10 +89,14 @@ 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) {
|
||||||
return showSelectFromStorageDialog(context);
|
if (context.mounted) {
|
||||||
|
return showSelectFromStorageDialog(context);
|
||||||
|
}
|
||||||
} else if (!await checkSplitApk(packageName) || isRooted) {
|
} else if (!await checkSplitApk(packageName) || isRooted) {
|
||||||
selectApp(app);
|
selectApp(app);
|
||||||
Navigator.pop(context);
|
if (context.mounted) {
|
||||||
|
Navigator.pop(context);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -198,7 +201,6 @@ class AppSelectorViewModel extends BaseViewModel {
|
|||||||
locator<PatcherViewModel>().selectedApp = PatchedApplication(
|
locator<PatcherViewModel>().selectedApp = PatchedApplication(
|
||||||
name: application.appName,
|
name: application.appName,
|
||||||
packageName: application.packageName,
|
packageName: application.packageName,
|
||||||
originalPackageName: application.packageName,
|
|
||||||
version: application.versionName!,
|
version: application.versionName!,
|
||||||
apkFilePath: result.files.single.path!,
|
apkFilePath: result.files.single.path!,
|
||||||
icon: application.icon,
|
icon: application.icon,
|
||||||
|
@ -37,7 +37,6 @@ class HomeViewModel extends BaseViewModel {
|
|||||||
DateTime? _lastUpdate;
|
DateTime? _lastUpdate;
|
||||||
bool showUpdatableApps = false;
|
bool showUpdatableApps = false;
|
||||||
List<PatchedApplication> patchedInstalledApps = [];
|
List<PatchedApplication> patchedInstalledApps = [];
|
||||||
List<PatchedApplication> patchedUpdatableApps = [];
|
|
||||||
String? _latestManagerVersion = '';
|
String? _latestManagerVersion = '';
|
||||||
File? downloadedApk;
|
File? downloadedApk;
|
||||||
|
|
||||||
@ -82,7 +81,7 @@ class HomeViewModel extends BaseViewModel {
|
|||||||
_toast.showBottom('homeView.errorDownloadMessage');
|
_toast.showBottom('homeView.errorDownloadMessage');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_getPatchedApps();
|
|
||||||
_managerAPI.reAssessSavedApps().then((_) => _getPatchedApps());
|
_managerAPI.reAssessSavedApps().then((_) => _getPatchedApps());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -108,10 +107,6 @@ class HomeViewModel extends BaseViewModel {
|
|||||||
|
|
||||||
void _getPatchedApps() {
|
void _getPatchedApps() {
|
||||||
patchedInstalledApps = _managerAPI.getPatchedApps().toList();
|
patchedInstalledApps = _managerAPI.getPatchedApps().toList();
|
||||||
patchedUpdatableApps = _managerAPI
|
|
||||||
.getPatchedApps()
|
|
||||||
.where((app) => app.hasUpdates == true)
|
|
||||||
.toList();
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -469,11 +464,7 @@ class HomeViewModel extends BaseViewModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> forceRefresh(BuildContext context) async {
|
Future<void> forceRefresh(BuildContext context) async {
|
||||||
await Future.delayed(const Duration(seconds: 1));
|
_managerAPI.clearAllData();
|
||||||
if (_lastUpdate == null ||
|
|
||||||
_lastUpdate!.difference(DateTime.now()).inSeconds > 2) {
|
|
||||||
_managerAPI.clearAllData();
|
|
||||||
}
|
|
||||||
_toast.showBottom('homeView.refreshSuccess');
|
_toast.showBottom('homeView.refreshSuccess');
|
||||||
initialize(context);
|
initialize(context);
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -130,28 +130,28 @@ class InstallerViewModel extends BaseViewModel {
|
|||||||
|
|
||||||
Future<void> runPatcher() async {
|
Future<void> runPatcher() async {
|
||||||
try {
|
try {
|
||||||
update(0.0, 'Initializing...', 'Initializing installer');
|
await _patcherAPI.runPatcher(
|
||||||
if (_patches.isNotEmpty) {
|
_app.packageName,
|
||||||
try {
|
_app.apkFilePath,
|
||||||
update(0.1, '', 'Creating working directory');
|
_patches,
|
||||||
await _patcherAPI.runPatcher(
|
);
|
||||||
_app.packageName,
|
} on Exception catch (e) {
|
||||||
_app.apkFilePath,
|
update(
|
||||||
_patches,
|
-100.0,
|
||||||
);
|
'Failed...',
|
||||||
} on Exception catch (e) {
|
'Something went wrong:\n$e',
|
||||||
update(
|
);
|
||||||
-100.0,
|
if (kDebugMode) {
|
||||||
'Aborted...',
|
print(e);
|
||||||
'An error occurred! Aborted\nError:\n$e',
|
|
||||||
);
|
|
||||||
if (kDebugMode) {
|
|
||||||
print(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
update(-100.0, 'Aborted...', 'No app or patches selected! Aborted');
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
if (FlutterBackground.isBackgroundExecutionEnabled) {
|
||||||
try {
|
try {
|
||||||
FlutterBackground.disableBackgroundExecution();
|
FlutterBackground.disableBackgroundExecution();
|
||||||
@ -182,52 +182,55 @@ 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(
|
||||||
valueListenable: installType,
|
child: ValueListenableBuilder(
|
||||||
builder: (context, value, child) {
|
valueListenable: installType,
|
||||||
return Column(
|
builder: (context, value, child) {
|
||||||
mainAxisSize: MainAxisSize.min,
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
Padding(
|
children: [
|
||||||
padding: const EdgeInsets.symmetric(
|
Padding(
|
||||||
horizontal: 20,
|
padding: const EdgeInsets.symmetric(
|
||||||
vertical: 10,
|
horizontal: 20,
|
||||||
),
|
vertical: 10,
|
||||||
child: I18nText(
|
),
|
||||||
'installerView.installTypeDescription',
|
child: I18nText(
|
||||||
child: Text(
|
'installerView.installTypeDescription',
|
||||||
'',
|
child: Text(
|
||||||
style: TextStyle(
|
'',
|
||||||
fontSize: 16,
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.w500,
|
fontSize: 16,
|
||||||
color: Theme.of(context).colorScheme.secondary,
|
fontWeight: FontWeight.w500,
|
||||||
|
color: Theme.of(context).colorScheme.secondary,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
RadioListTile(
|
||||||
RadioListTile(
|
title: I18nText('installerView.installNonRootType'),
|
||||||
title: I18nText('installerView.installNonRootType'),
|
contentPadding:
|
||||||
subtitle: I18nText('installerView.installRecommendedType'),
|
const EdgeInsets.symmetric(horizontal: 16),
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 10),
|
value: 0,
|
||||||
value: 0,
|
groupValue: value,
|
||||||
groupValue: value,
|
onChanged: (selected) {
|
||||||
onChanged: (selected) {
|
installType.value = selected!;
|
||||||
installType.value = selected!;
|
},
|
||||||
},
|
),
|
||||||
),
|
RadioListTile(
|
||||||
RadioListTile(
|
title: I18nText('installerView.installRootType'),
|
||||||
title: I18nText('installerView.installRootType'),
|
contentPadding:
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 10),
|
const EdgeInsets.symmetric(horizontal: 16),
|
||||||
value: 1,
|
value: 1,
|
||||||
groupValue: value,
|
groupValue: value,
|
||||||
onChanged: (selected) {
|
onChanged: (selected) {
|
||||||
installType.value = selected!;
|
installType.value = selected!;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
CustomMaterialButton(
|
CustomMaterialButton(
|
||||||
@ -255,9 +258,9 @@ class InstallerViewModel extends BaseViewModel {
|
|||||||
Future<void> stopPatcher() async {
|
Future<void> stopPatcher() async {
|
||||||
try {
|
try {
|
||||||
isCanceled = true;
|
isCanceled = true;
|
||||||
update(0.5, 'Aborting...', 'Canceling patching process');
|
update(0.5, 'Canceling...', 'Canceling patching process');
|
||||||
await _patcherAPI.stopPatcher();
|
await _patcherAPI.stopPatcher();
|
||||||
update(-100.0, 'Aborted...', 'Press back to exit');
|
update(-100.0, 'Canceled...', 'Press back to exit');
|
||||||
} on Exception catch (e) {
|
} on Exception catch (e) {
|
||||||
if (kDebugMode) {
|
if (kDebugMode) {
|
||||||
print(e);
|
print(e);
|
||||||
@ -268,56 +271,33 @@ class InstallerViewModel extends BaseViewModel {
|
|||||||
Future<void> installResult(BuildContext context, bool installAsRoot) async {
|
Future<void> installResult(BuildContext context, bool installAsRoot) async {
|
||||||
try {
|
try {
|
||||||
_app.isRooted = installAsRoot;
|
_app.isRooted = installAsRoot;
|
||||||
final bool hasMicroG =
|
update(
|
||||||
_patches.any((p) => p.name.endsWith('MicroG support'));
|
1.0,
|
||||||
final bool rootMicroG = installAsRoot && hasMicroG;
|
'Installing...',
|
||||||
final bool rootFromStorage = installAsRoot && _app.isFromStorage;
|
_app.isRooted
|
||||||
final bool ytWithoutRootMicroG =
|
? 'Installing patched file using root method'
|
||||||
!installAsRoot && !hasMicroG && _app.packageName.contains('youtube');
|
: 'Installing patched file using nonroot method',
|
||||||
if (rootMicroG || rootFromStorage || ytWithoutRootMicroG) {
|
);
|
||||||
return showDialog(
|
isInstalled = await _patcherAPI.installPatchedFile(_app);
|
||||||
context: context,
|
if (isInstalled) {
|
||||||
builder: (context) => AlertDialog(
|
_app.isFromStorage = false;
|
||||||
title: I18nText('installerView.installErrorDialogTitle'),
|
_app.patchDate = DateTime.now();
|
||||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
_app.appliedPatches = _patches.map((p) => p.name).toList();
|
||||||
content: I18nText(
|
|
||||||
rootMicroG
|
// In case a patch changed the app name or package name,
|
||||||
? 'installerView.installErrorDialogText1'
|
// update the app info.
|
||||||
: rootFromStorage
|
final app =
|
||||||
? 'installerView.installErrorDialogText3'
|
await DeviceApps.getAppFromStorage(_patcherAPI.outFile!.path);
|
||||||
: 'installerView.installErrorDialogText2',
|
if (app != null) {
|
||||||
),
|
_app.name = app.appName;
|
||||||
actions: <Widget>[
|
_app.packageName = app.packageName;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await _managerAPI.savePatchedApp(_app);
|
||||||
|
|
||||||
|
update(1.0, 'Installed!', 'Installed!');
|
||||||
|
} else {
|
||||||
|
// TODO(aabed): Show error message.
|
||||||
}
|
}
|
||||||
} on Exception catch (e) {
|
} on Exception catch (e) {
|
||||||
if (kDebugMode) {
|
if (kDebugMode) {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
// ignore_for_file: use_build_context_synchronously
|
// 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:dynamic_themes/dynamic_themes.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
@ -18,25 +19,35 @@ 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) {
|
final dynamicTheme = DynamicTheme.of(context)!;
|
||||||
final bool isDark =
|
if (prefs.getInt('themeMode') == null) {
|
||||||
MediaQuery.platformBrightnessOf(context) != Brightness.light;
|
await prefs.setInt('themeMode', 0);
|
||||||
await prefs.setBool('useDarkTheme', isDark);
|
await dynamicTheme.setTheme(0);
|
||||||
await DynamicTheme.of(context)!.setTheme(isDark ? 1 : 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.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
||||||
SystemChrome.setSystemUIOverlayStyle(
|
SystemChrome.setSystemUIOverlayStyle(
|
||||||
SystemUiOverlayStyle(
|
SystemUiOverlayStyle(
|
||||||
|
@ -44,49 +44,6 @@ class PatcherViewModel extends BaseViewModel {
|
|||||||
return selectedApp == null;
|
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 {
|
Future<void> showRemovedPatchesDialog(BuildContext context) async {
|
||||||
if (removedPatches.isNotEmpty) {
|
if (removedPatches.isNotEmpty) {
|
||||||
return showDialog(
|
return showDialog(
|
||||||
@ -115,7 +72,7 @@ class PatcherViewModel extends BaseViewModel {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
showArmv7WarningDialog(context);
|
showArmv7WarningDialog(context); // TODO(aabed): Find out why this is here
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -185,9 +142,9 @@ class PatcherViewModel extends BaseViewModel {
|
|||||||
this.selectedPatches.clear();
|
this.selectedPatches.clear();
|
||||||
removedPatches.clear();
|
removedPatches.clear();
|
||||||
final List<String> selectedPatches =
|
final List<String> selectedPatches =
|
||||||
await _managerAPI.getSelectedPatches(selectedApp!.originalPackageName);
|
await _managerAPI.getSelectedPatches(selectedApp!.packageName);
|
||||||
final List<Patch> patches =
|
final List<Patch> patches =
|
||||||
_patcherAPI.getFilteredPatches(selectedApp!.originalPackageName);
|
_patcherAPI.getFilteredPatches(selectedApp!.packageName);
|
||||||
this
|
this
|
||||||
.selectedPatches
|
.selectedPatches
|
||||||
.addAll(patches.where((patch) => selectedPatches.contains(patch.name)));
|
.addAll(patches.where((patch) => selectedPatches.contains(patch.name)));
|
||||||
@ -203,7 +160,7 @@ class PatcherViewModel extends BaseViewModel {
|
|||||||
.selectedPatches
|
.selectedPatches
|
||||||
.removeWhere((patch) => patch.compatiblePackages.isEmpty);
|
.removeWhere((patch) => patch.compatiblePackages.isEmpty);
|
||||||
}
|
}
|
||||||
final usedPatches = _managerAPI.getUsedPatches(selectedApp!.originalPackageName);
|
final usedPatches = _managerAPI.getUsedPatches(selectedApp!.packageName);
|
||||||
for (final patch in usedPatches){
|
for (final patch in usedPatches){
|
||||||
if (!patches.any((p) => p.name == patch.name)){
|
if (!patches.any((p) => p.name == patch.name)){
|
||||||
removedPatches.add('\u2022 ${patch.name}');
|
removedPatches.add('\u2022 ${patch.name}');
|
||||||
|
@ -81,7 +81,7 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
|
|||||||
child: Container(
|
child: Container(
|
||||||
margin: const EdgeInsets.only(top: 12, bottom: 12),
|
margin: const EdgeInsets.only(top: 12, bottom: 12),
|
||||||
padding:
|
padding:
|
||||||
const EdgeInsets.symmetric(horizontal: 6, vertical: 6),
|
const EdgeInsets.symmetric(horizontal: 6, vertical: 6),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Theme.of(context)
|
color: Theme.of(context)
|
||||||
.colorScheme
|
.colorScheme
|
||||||
@ -99,7 +99,7 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
|
|||||||
),
|
),
|
||||||
CustomPopupMenu(
|
CustomPopupMenu(
|
||||||
onSelected: (value) =>
|
onSelected: (value) =>
|
||||||
{model.onMenuSelection(value, context)},
|
{model.onMenuSelection(value, context)},
|
||||||
children: {
|
children: {
|
||||||
0: I18nText(
|
0: I18nText(
|
||||||
'patchesSelectorView.loadPatchesSelection',
|
'patchesSelectorView.loadPatchesSelection',
|
||||||
@ -114,7 +114,7 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
bottom: PreferredSize(
|
bottom: PreferredSize(
|
||||||
preferredSize: const Size.fromHeight(64.0),
|
preferredSize: const Size.fromHeight(66.0),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
vertical: 8.0,
|
vertical: 8.0,
|
||||||
@ -194,12 +194,13 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
|
|||||||
return PatchItem(
|
return PatchItem(
|
||||||
name: patch.name,
|
name: patch.name,
|
||||||
simpleName: patch.getSimpleName(),
|
simpleName: patch.getSimpleName(),
|
||||||
description: patch.description,
|
description: patch.description ?? '',
|
||||||
packageVersion: model.getAppInfo().version,
|
packageVersion: model.getAppInfo().version,
|
||||||
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: I18nText(
|
child: Container(
|
||||||
'patchesSelectorView.universalPatches',
|
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) {
|
...model.getQueriedPatches(_query).map((patch) {
|
||||||
@ -230,13 +246,14 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
|
|||||||
return PatchItem(
|
return PatchItem(
|
||||||
name: patch.name,
|
name: patch.name,
|
||||||
simpleName: patch.getSimpleName(),
|
simpleName: patch.getSimpleName(),
|
||||||
description: patch.description,
|
description: patch.description ?? '',
|
||||||
packageVersion:
|
packageVersion:
|
||||||
model.getAppInfo().version,
|
model.getAppInfo().version,
|
||||||
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(
|
||||||
|
@ -28,7 +28,7 @@ class PatchesSelectorViewModel extends BaseViewModel {
|
|||||||
getPatchesVersion().whenComplete(() => notifyListeners());
|
getPatchesVersion().whenComplete(() => notifyListeners());
|
||||||
patches.addAll(
|
patches.addAll(
|
||||||
_patcherAPI.getFilteredPatches(
|
_patcherAPI.getFilteredPatches(
|
||||||
selectedApp!.originalPackageName,
|
selectedApp!.packageName,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
patches.sort((a, b) {
|
patches.sort((a, b) {
|
||||||
@ -98,11 +98,11 @@ class PatchesSelectorViewModel extends BaseViewModel {
|
|||||||
|
|
||||||
void selectDefaultPatches() {
|
void selectDefaultPatches() {
|
||||||
selectedPatches.clear();
|
selectedPatches.clear();
|
||||||
if (locator<PatcherViewModel>().selectedApp?.originalPackageName != null) {
|
if (locator<PatcherViewModel>().selectedApp?.packageName != null) {
|
||||||
selectedPatches.addAll(
|
selectedPatches.addAll(
|
||||||
_patcherAPI
|
_patcherAPI
|
||||||
.getFilteredPatches(
|
.getFilteredPatches(
|
||||||
locator<PatcherViewModel>().selectedApp!.originalPackageName,
|
locator<PatcherViewModel>().selectedApp!.packageName,
|
||||||
)
|
)
|
||||||
.where(
|
.where(
|
||||||
(element) =>
|
(element) =>
|
||||||
@ -187,7 +187,7 @@ class PatchesSelectorViewModel extends BaseViewModel {
|
|||||||
final List<String> selectedPatches =
|
final List<String> selectedPatches =
|
||||||
this.selectedPatches.map((patch) => patch.name).toList();
|
this.selectedPatches.map((patch) => patch.name).toList();
|
||||||
await _managerAPI.setSelectedPatches(
|
await _managerAPI.setSelectedPatches(
|
||||||
locator<PatcherViewModel>().selectedApp!.originalPackageName,
|
locator<PatcherViewModel>().selectedApp!.packageName,
|
||||||
selectedPatches,
|
selectedPatches,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -195,7 +195,7 @@ class PatchesSelectorViewModel extends BaseViewModel {
|
|||||||
Future<void> loadSelectedPatches(BuildContext context) async {
|
Future<void> loadSelectedPatches(BuildContext context) async {
|
||||||
if (_managerAPI.isPatchesChangeEnabled()) {
|
if (_managerAPI.isPatchesChangeEnabled()) {
|
||||||
final List<String> selectedPatches = await _managerAPI.getSelectedPatches(
|
final List<String> selectedPatches = await _managerAPI.getSelectedPatches(
|
||||||
locator<PatcherViewModel>().selectedApp!.originalPackageName,
|
locator<PatcherViewModel>().selectedApp!.packageName,
|
||||||
);
|
);
|
||||||
if (selectedPatches.isNotEmpty) {
|
if (selectedPatches.isNotEmpty) {
|
||||||
this.selectedPatches.clear();
|
this.selectedPatches.clear();
|
||||||
|
@ -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(),
|
||||||
|
@ -71,12 +71,6 @@ class SettingsViewModel extends BaseViewModel {
|
|||||||
actions: [
|
actions: [
|
||||||
CustomMaterialButton(
|
CustomMaterialButton(
|
||||||
isFilled: false,
|
isFilled: false,
|
||||||
label: I18nText('noButton'),
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
CustomMaterialButton(
|
|
||||||
label: I18nText('yesButton'),
|
label: I18nText('yesButton'),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
_managerAPI.setChangingToggleModified(true);
|
_managerAPI.setChangingToggleModified(true);
|
||||||
@ -84,6 +78,12 @@ class SettingsViewModel extends BaseViewModel {
|
|||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
CustomMaterialButton(
|
||||||
|
label: I18nText('noButton'),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -222,22 +222,6 @@ class AppInfoView extends StatelessWidget {
|
|||||||
subtitle: Text(app.packageName),
|
subtitle: Text(app.packageName),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
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(
|
ListTile(
|
||||||
contentPadding:
|
contentPadding:
|
||||||
const EdgeInsets.symmetric(horizontal: 20.0),
|
const EdgeInsets.symmetric(horizontal: 20.0),
|
||||||
|
@ -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/navigation/navigation_viewmodel.dart';
|
||||||
import 'package:revanced_manager/ui/views/patcher/patcher_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/ui/widgets/shared/custom_material_button.dart';
|
||||||
import 'package:revanced_manager/utils/string.dart';
|
|
||||||
import 'package:stacked/stacked.dart';
|
import 'package:stacked/stacked.dart';
|
||||||
|
|
||||||
class AppInfoViewModel extends BaseViewModel {
|
class AppInfoViewModel extends BaseViewModel {
|
||||||
@ -147,17 +146,7 @@ class AppInfoViewModel extends BaseViewModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String getAppliedPatchesString(List<String> appliedPatches) {
|
String getAppliedPatchesString(List<String> appliedPatches) {
|
||||||
final List<String> names = appliedPatches
|
return '\u2022 ${appliedPatches.join('\n\u2022 ')}';
|
||||||
.map(
|
|
||||||
(p) => p
|
|
||||||
.replaceAll('-', ' ')
|
|
||||||
.split('-')
|
|
||||||
.join(' ')
|
|
||||||
.toTitleCase()
|
|
||||||
.replaceFirst('Microg', 'MicroG'),
|
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
return '\u2022 ${names.join('\n\u2022 ')}';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void openApp(PatchedApplication app) {
|
void openApp(PatchedApplication app) {
|
||||||
|
@ -79,8 +79,6 @@ class InstalledAppsCard extends StatelessWidget {
|
|||||||
icon: app.icon,
|
icon: app.icon,
|
||||||
name: app.name,
|
name: app.name,
|
||||||
patchDate: app.patchDate,
|
patchDate: app.patchDate,
|
||||||
changelog: app.changelog,
|
|
||||||
isUpdatableApp: false,
|
|
||||||
onPressed: () =>
|
onPressed: () =>
|
||||||
locator<HomeViewModel>().navigateToAppInfo(app),
|
locator<HomeViewModel>().navigateToAppInfo(app),
|
||||||
),
|
),
|
||||||
|
@ -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_api_url.dart';
|
||||||
import 'package:revanced_manager/ui/views/settings/settingsFragment/settings_manage_sources.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/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_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_patches.dart';
|
||||||
import 'package:revanced_manager/ui/widgets/settingsView/settings_experimental_universal_patches.dart';
|
import 'package:revanced_manager/ui/widgets/settingsView/settings_experimental_universal_patches.dart';
|
||||||
import 'package:revanced_manager/ui/widgets/settingsView/settings_section.dart';
|
import 'package:revanced_manager/ui/widgets/settingsView/settings_section.dart';
|
||||||
|
@ -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',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:expandable/expandable.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_i18n/flutter_i18n.dart';
|
import 'package:flutter_i18n/flutter_i18n.dart';
|
||||||
import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
|
import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
|
||||||
@ -13,151 +12,84 @@ class ApplicationItem extends StatefulWidget {
|
|||||||
required this.icon,
|
required this.icon,
|
||||||
required this.name,
|
required this.name,
|
||||||
required this.patchDate,
|
required this.patchDate,
|
||||||
required this.changelog,
|
|
||||||
required this.isUpdatableApp,
|
|
||||||
required this.onPressed,
|
required this.onPressed,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
final Uint8List icon;
|
final Uint8List icon;
|
||||||
final String name;
|
final String name;
|
||||||
final DateTime patchDate;
|
final DateTime patchDate;
|
||||||
final List<String> changelog;
|
|
||||||
final bool isUpdatableApp;
|
|
||||||
final Function() onPressed;
|
final Function() onPressed;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ApplicationItem> createState() => _ApplicationItemState();
|
State<ApplicationItem> createState() => _ApplicationItemState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ApplicationItemState extends State<ApplicationItem>
|
class _ApplicationItemState extends State<ApplicationItem> {
|
||||||
with TickerProviderStateMixin {
|
|
||||||
late AnimationController _animationController;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_animationController = AnimationController(
|
|
||||||
vsync: this,
|
|
||||||
duration: const Duration(milliseconds: 300),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_animationController.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final ExpandableController expController = ExpandableController();
|
|
||||||
return Container(
|
return Container(
|
||||||
margin: const EdgeInsets.only(bottom: 16.0),
|
margin: const EdgeInsets.only(bottom: 16.0),
|
||||||
child: CustomCard(
|
child: CustomCard(
|
||||||
onTap: () {
|
child: Row(
|
||||||
expController.toggle();
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
_animationController.isCompleted
|
children: [
|
||||||
? _animationController.reverse()
|
Flexible(
|
||||||
: _animationController.forward();
|
child: Row(
|
||||||
},
|
|
||||||
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(
|
|
||||||
children: [
|
children: [
|
||||||
RotationTransition(
|
SizedBox(
|
||||||
turns: Tween(begin: 0.0, end: 0.50)
|
width: 40,
|
||||||
.animate(_animationController),
|
child: Image.memory(widget.icon, height: 40, width: 40),
|
||||||
child: const Padding(
|
|
||||||
padding: EdgeInsets.all(8.0),
|
|
||||||
child: Icon(Icons.arrow_drop_down),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 19),
|
||||||
Column(
|
Expanded(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
CustomMaterialButton(
|
Text(
|
||||||
label: widget.isUpdatableApp
|
widget.name,
|
||||||
? I18nText('applicationItem.patchButton')
|
maxLines: 1,
|
||||||
: I18nText('applicationItem.infoButton'),
|
overflow: TextOverflow.ellipsis,
|
||||||
onPressed: widget.onPressed,
|
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(
|
Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
children: [
|
||||||
children: <Widget>[
|
const SizedBox(width: 8),
|
||||||
I18nText(
|
Column(
|
||||||
'applicationItem.changelogLabel',
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
child: const Text(
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
'',
|
children: <Widget>[
|
||||||
style: TextStyle(fontWeight: FontWeight.w700),
|
CustomMaterialButton(
|
||||||
),
|
label: I18nText('applicationItem.infoButton'),
|
||||||
|
onPressed: widget.onPressed,
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
|
||||||
Text('\u2022 ${widget.changelog.join('\n\u2022 ')}'),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -4,7 +4,7 @@ homepage: https://github.com/revanced/revanced-manager
|
|||||||
|
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
|
|
||||||
version: 1.9.2+100900200
|
version: 1.11.1+101100100
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.0.0 <4.0.0'
|
sdk: '>=3.0.0 <4.0.0'
|
||||||
@ -75,6 +75,7 @@ dependencies:
|
|||||||
flutter_markdown: ^0.6.14
|
flutter_markdown: ^0.6.14
|
||||||
dio_cache_interceptor: ^3.4.0
|
dio_cache_interceptor: ^3.4.0
|
||||||
install_plugin: ^2.1.0
|
install_plugin: ^2.1.0
|
||||||
|
synchronized: ^3.1.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
json_serializable: ^6.6.1
|
json_serializable: ^6.6.1
|
||||||
|
Reference in New Issue
Block a user