Compare commits

..

1 Commits

Author SHA1 Message Date
Ushie
c0446eac35 build: bump version to v1.9.5 2023-09-04 04:15:55 +03:00
35 changed files with 782 additions and 751 deletions

View File

@@ -12,7 +12,7 @@ body:
- type: textarea - type: textarea
attributes: attributes:
label: Bug description label: Bug description
description: | description:
- Describe your bug in detail - Describe your bug in detail
- Add steps to reproduce the bug if possible (Step 1. Download some files. Step 2. ...) - Add steps to reproduce the bug if possible (Step 1. Download some files. Step 2. ...)
- Add images and videos if possible - Add images and videos if possible

38
.github/workflows/analyze.yml vendored Normal file
View File

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

View File

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

View File

@@ -9,7 +9,7 @@ jobs:
release: release:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- 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

View File

@@ -3,33 +3,29 @@
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. Incompatible with certain ARMv7 devices 2. Does not work on some 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).
## 🌐 Translation ## 💭 Discussion
If you wish to discuss the Manager, a thread has been made under the [#development](https://discord.com/channels/952946952348270622/1002922226443632761) channel in the Discord server, please note that this thread may be temporary and may be removed in the future.
## 🌐 Translation
[![Crowdin](https://badges.crowdin.net/revanced/localized.svg)](https://crowdin.com/project/revanced) [![Crowdin](https://badges.crowdin.net/revanced/localized.svg)](https://crowdin.com/project/revanced)
We're accepting translations on [Crowdin](https://translate.revanced.app). If you wish to translate ReVanced Manager, we're accepting translations on [Crowdin](https://translate.revanced.app)
## 🛠️ Building Manager from source ## 🛠️ Building Manager from source
1. Setup flutter environment for your [platform](https://docs.flutter.dev/get-started/install) 1. Setup flutter environment for your [platform](https://docs.flutter.dev/get-started/install)
2. Clone the repository locally 2. Clone the repository locally
3. Add your GitHub token in gradle.properties like [this](/docs/4_building.md) 3. Add your github token in gradle.properties like [this](/docs/4_building.md)
4. Open the project in terminal 4. Open the project in terminal
5. Run `flutter pub get` in terminal 5. Run `flutter pub get` in terminal
6. Then `flutter packages pub run build_runner build --delete-conflicting-outputs` (Must be done on each git pull) 6. Then `flutter packages pub run build_runner build --delete-conflicting-outputs` (Must be done on each git pull)
7. To build release APK run `flutter build apk` 7. To build release apk run `flutter build apk`

View File

@@ -85,7 +85,7 @@ 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:16.0.2" implementation "app.revanced:revanced-patcher:14.2.2"
// Signing & aligning // Signing & aligning
implementation("org.bouncycastle:bcpkix-jdk15on:1.70") implementation("org.bouncycastle:bcpkix-jdk15on:1.70")

View File

@@ -25,7 +25,8 @@
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"
@@ -42,10 +43,6 @@
<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" />

View File

@@ -1,83 +0,0 @@
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.", ""), value)
}
// Load keystore
val keystoreFile = File(getExternalFilesDir(null), "/revanced-manager.keystore")
if (keystoreFile.exists()) {
val keystoreBytes = keystoreFile.readBytes()
val keystoreBase64 = Base64.encodeToString(keystoreBytes, Base64.DEFAULT)
json.put("keystore", keystoreBase64)
}
// Load saved patches
val storedPatchesFile = File(filesDir.parentFile.absolutePath, "/app_flutter/selected-patches.json")
if (storedPatchesFile.exists()) {
val patchesBytes = storedPatchesFile.readBytes()
val patches = String(patchesBytes, Charsets.UTF_8)
json.put("patches", JSONObject(patches))
}
// Send data back
val resultIntent = Intent()
resultIntent.putExtra("data", json.toString())
setResult(Activity.RESULT_OK, resultIntent)
finish()
} else {
val resultIntent = Intent()
setResult(Activity.RESULT_CANCELED)
finish()
}
}
fun getFingerprint(packageName: String): String {
// Get the signature of the app that matches the package name
val packageInfo = packageManager.getPackageInfo(packageName, PackageManager.GET_SIGNATURES)
val signature = packageInfo.signatures[0]
// Get the raw certificate data
val rawCert = signature.toByteArray()
// Generate an X509Certificate from the data
val certFactory = CertificateFactory.getInstance("X509")
val x509Cert = certFactory.generateCertificate(ByteArrayInputStream(rawCert)) as X509Certificate
// Get the SHA256 fingerprint
val fingerprint = MessageDigest.getInstance("SHA256").digest(x509Cert.encoded).joinToString("") {
"%02x".format(it)
}
return fingerprint
}
}

View File

@@ -8,21 +8,19 @@ 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.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.extensions.PatchExtensions.patchName
import app.revanced.patcher.patch.PatchResult import app.revanced.patcher.patch.PatchResult
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.cancel
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.json.JSONArray
import org.json.JSONObject
import java.io.File import java.io.File
import java.io.PrintWriter import java.io.PrintWriter
import java.io.StringWriter import java.io.StringWriter
import java.lang.Error
import java.util.logging.LogRecord import java.util.logging.LogRecord
import java.util.logging.Logger import java.util.logging.Logger
@@ -32,8 +30,6 @@ 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
private lateinit var patches: PatchSet
override fun configureFlutterEngine(flutterEngine: FlutterEngine) { override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine) super.configureFlutterEngine(flutterEngine)
@@ -49,6 +45,7 @@ class MainActivity : FlutterActivity() {
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")
@@ -59,7 +56,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 ( if (patchBundleFilePath != null &&
originalFilePath != null && originalFilePath != null &&
inputFilePath != null && inputFilePath != null &&
patchedFilePath != null && patchedFilePath != null &&
@@ -73,6 +70,7 @@ class MainActivity : FlutterActivity() {
cancel = false cancel = false
runPatcher( runPatcher(
result, result,
patchBundleFilePath,
originalFilePath, originalFilePath,
inputFilePath, inputFilePath,
patchedFilePath, patchedFilePath,
@@ -91,47 +89,6 @@ 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()
} }
} }
@@ -139,6 +96,7 @@ 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,
@@ -181,11 +139,8 @@ class MainActivity : FlutterActivity() {
} }
object : java.util.logging.Handler() { object : java.util.logging.Handler() {
override fun publish(record: LogRecord) { override fun publish(record: LogRecord) =
if (record.loggerName?.startsWith("app.revanced") != true) return
updateProgress(-1.0, "", record.message) updateProgress(-1.0, "", record.message)
}
override fun flush() = Unit override fun flush() = Unit
override fun close() = flush() override fun close() = flush()
@@ -225,7 +180,10 @@ class MainActivity : FlutterActivity() {
updateProgress(0.1, "Loading patches...", "Loading patches") updateProgress(0.1, "Loading patches...", "Loading patches")
val patches = patches.filter { patch -> val patches = PatchBundleLoader.Dex(
File(patchBundleFilePath),
optimizedDexDirectory = cacheDir
).filter { patch ->
val isCompatible = patch.compatiblePackages?.any { val isCompatible = patch.compatiblePackages?.any {
it.name == patcher.context.packageMetadata.packageName it.name == patcher.context.packageMetadata.packageName
} ?: false } ?: false
@@ -233,7 +191,7 @@ class MainActivity : FlutterActivity() {
val compatibleOrUniversal = val compatibleOrUniversal =
isCompatible || patch.compatiblePackages.isNullOrEmpty() isCompatible || patch.compatiblePackages.isNullOrEmpty()
compatibleOrUniversal && selectedPatches.any { it == patch.name } compatibleOrUniversal && selectedPatches.any { it == patch.patchName }
} }
if (cancel) { if (cancel) {
@@ -264,9 +222,9 @@ class MainActivity : FlutterActivity() {
val msg = patchResult.exception?.let { val msg = patchResult.exception?.let {
val writer = StringWriter() val writer = StringWriter()
it.printStackTrace(PrintWriter(writer)) it.printStackTrace(PrintWriter(writer))
"${patchResult.patch.name} failed: $writer" "${patchResult.patchName} failed: $writer"
} ?: run { } ?: run {
"${patchResult.patch.name} succeeded" "${patchResult.patchName} succeeded"
} }
updateProgress(progress, "", msg) updateProgress(progress, "", msg)
@@ -330,7 +288,7 @@ class MainActivity : FlutterActivity() {
val stack = ex.stackTraceToString() val stack = ex.stackTraceToString()
updateProgress( updateProgress(
-100.0, -100.0,
"Failed", "Aborted",
"An error occurred:\n$stack" "An error occurred:\n$stack"
) )
} }

View File

@@ -23,13 +23,13 @@
"widgetTitle": "Dashboard", "widgetTitle": "Dashboard",
"updatesSubtitle": "Updates", "updatesSubtitle": "Updates",
"patchedSubtitle": "Patched apps", "patchedSubtitle": "Patched applications",
"noUpdates": "No updates available", "noUpdates": "No updates available",
"WIP": "Work in progress...", "WIP": "Work in progress...",
"noInstallations": "No patched apps installed", "noInstallations": "No patched applications installed",
"installUpdate": "Continue to install the update?", "installUpdate": "Continue to install the update?",
"updateDialogTitle": "Update Manager", "updateDialogTitle": "Update Manager",
@@ -56,7 +56,9 @@
"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": {
"infoButton": "Info" "patchButton": "Patch",
"infoButton": "Info",
"changelogLabel": "Changelog"
}, },
"latestCommitCard": { "latestCommitCard": {
"loadingLabel": "Loading...", "loadingLabel": "Loading...",
@@ -69,8 +71,9 @@
"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": {
@@ -145,8 +148,9 @@
"installTypeDescription": "Select the installation type to proceed with.", "installTypeDescription": "Select the installation type to proceed with.",
"installButton": "Install", "installButton": "Install",
"installRootType": "Mount", "installRootType": "Root",
"installNonRootType": "Normal", "installNonRootType": "Non-root",
"installRecommendedType": "Recommended",
"pressBackAgain": "Press back again to cancel", "pressBackAgain": "Press back again to cancel",
"openButton": "Open", "openButton": "Open",
@@ -158,9 +162,10 @@
"exportApkButtonTooltip": "Export patched APK", "exportApkButtonTooltip": "Export patched APK",
"exportLogButtonTooltip": "Export log", "exportLogButtonTooltip": "Export log",
"screenshotDetected": "A screenshot has been detected. If you are trying to share the log, please share a text copy instead.\n\nCopy log to clipboard?", "installErrorDialogTitle": "Error",
"copiedToClipboard": "Copied log to clipboard", "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": {
@@ -173,10 +178,8 @@
"exportSectionTitle": "Import & export", "exportSectionTitle": "Import & export",
"logsSectionTitle": "Logs", "logsSectionTitle": "Logs",
"themeModeLabel": "App theme", "darkThemeLabel": "Dark mode",
"systemThemeLabel": "System", "darkThemeHint": "Welcome to the dark side",
"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",
@@ -280,6 +283,7 @@
"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",

View File

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

View File

@@ -1,4 +1,5 @@
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';
@@ -8,19 +9,26 @@ 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');
} }
} }

View File

@@ -9,19 +9,23 @@ 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(
@@ -32,7 +36,9 @@ 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);

View File

@@ -1,3 +1,4 @@
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';
@@ -6,6 +7,7 @@ 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
@@ -19,6 +21,17 @@ 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(
@@ -129,6 +142,38 @@ 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,
@@ -177,8 +222,10 @@ 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); _managerAPI.setPatchesDownloadURL(downloadUrl, true);
} }
return await DefaultCacheManager().getSingleFile( return await DefaultCacheManager().getSingleFile(
downloadUrl, downloadUrl,
@@ -192,4 +239,30 @@ 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;
}
} }

View File

@@ -11,7 +11,6 @@ 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';
@@ -27,7 +26,6 @@ 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 =
@@ -42,14 +40,12 @@ 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().toLowerCase() == 'revanced/revanced-patches'; return getPatchesRepo() == 'revanced/revanced-patches';
} }
bool isDefaultIntegrationsRepo() { bool isDefaultIntegrationsRepo() {
return getIntegrationsRepo().toLowerCase() == return getIntegrationsRepo() == 'revanced/revanced-integrations';
'revanced/revanced-integrations';
} }
Future<void> initialize() async { Future<void> initialize() async {
@@ -83,12 +79,12 @@ class ManagerAPI {
await _prefs.setString('repoUrl', url); await _prefs.setString('repoUrl', url);
} }
String getPatchesDownloadURL() { String getPatchesDownloadURL(bool bundle) {
return _prefs.getString('patchesDownloadURL') ?? ''; return _prefs.getString('patchesDownloadURL-$bundle') ?? '';
} }
Future<void> setPatchesDownloadURL(String value) async { Future<void> setPatchesDownloadURL(String value, bool bundle) async {
await _prefs.setString('patchesDownloadURL', value); await _prefs.setString('patchesDownloadURL-$bundle', value);
} }
String getPatchesRepo() { String getPatchesRepo() {
@@ -201,12 +197,12 @@ class ManagerAPI {
await _prefs.setBool('useDynamicTheme', value); await _prefs.setBool('useDynamicTheme', value);
} }
int getThemeMode() { bool getUseDarkTheme() {
return _prefs.getInt('themeMode') ?? 2; return _prefs.getBool('useDarkTheme') ?? false;
} }
Future<void> setThemeMode(int value) async { Future<void> setUseDarkTheme(bool value) async {
await _prefs.setInt('themeMode', value); await _prefs.setBool('useDarkTheme', value);
} }
bool areUniversalPatchesEnabled() { bool areUniversalPatchesEnabled() {
@@ -304,46 +300,28 @@ class ManagerAPI {
} }
Future<List<Patch>> getPatches() async { Future<List<Patch>> getPatches() async {
if (patches.isNotEmpty) { try {
return patches; final String repoName = getPatchesRepo();
} final String currentVersion = await getCurrentPatchesVersion();
final File? patchBundleFile = await downloadPatches(); final String url = getPatchesDownloadURL(false);
final Directory appCache = await getTemporaryDirectory(); return await _githubAPI.getPatches(
Directory('${appCache.path}/cache').createSync(); repoName,
final Directory workDir = currentVersion,
Directory('${appCache.path}/cache').createTempSync('tmp-'); url,
final Directory cacheDir = Directory('${workDir.path}/cache'); );
cacheDir.createSync(); } on Exception catch (e) {
if (kDebugMode) {
if (patchBundleFile != null) { print(e);
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 [];
} }
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(); final String url = getPatchesDownloadURL(true);
return await _githubAPI.getPatchesReleaseFile( return await _githubAPI.getPatchesReleaseFile(
'.jar', '.jar',
repoName, repoName,
@@ -469,7 +447,8 @@ 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(''); await setPatchesDownloadURL('', false);
await setPatchesDownloadURL('', true);
await downloadPatches(); await downloadPatches();
} }
@@ -505,33 +484,62 @@ class ManagerAPI {
return toRemove; return toRemove;
} }
Future<List<PatchedApplication>> getMountedApps() async { Future<List<PatchedApplication>> getUnsavedApps(
final List<PatchedApplication> mountedApps = []; List<PatchedApplication> patchedApps,
) 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(
packageName, app.packageName,
true, true,
) as ApplicationWithIcon?; ) as ApplicationWithIcon?;
if (application != null) { if (application != null) {
mountedApps.add( unsavedApps.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) {
@@ -593,20 +601,34 @@ class ManagerAPI {
Future<void> reAssessSavedApps() async { Future<void> reAssessSavedApps() async {
final List<PatchedApplication> patchedApps = getPatchedApps(); final List<PatchedApplication> patchedApps = getPatchedApps();
final List<PatchedApplication> unsavedApps =
// Remove apps that are not installed anymore. await getUnsavedApps(patchedApps);
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) {
// Determine all apps that are installed by mounting. app.hasUpdates =
final List<PatchedApplication> mountedApps = await getMountedApps(); await hasAppUpdates(app.originalPackageName, app.patchDate);
mountedApps.removeWhere( app.changelog =
(app) => patchedApps await getAppChangelog(app.originalPackageName, app.patchDate);
.any((patchedApp) => patchedApp.packageName == app.packageName), if (!app.hasUpdates) {
); final String? currentInstalledVersion =
patchedApps.addAll(mountedApps); (await DeviceApps.getApp(app.packageName))?.versionName;
if (currentInstalledVersion != null) {
final String currentSavedVersion = app.version;
final int currentInstalledVersionInt = int.parse(
currentInstalledVersion.replaceAll(RegExp('[^0-9]'), ''),
);
final int currentSavedVersionInt = int.parse(
currentSavedVersion.replaceAll(RegExp('[^0-9]'), ''),
);
if (currentInstalledVersionInt > currentSavedVersionInt) {
app.hasUpdates = true;
}
}
}
}
await setPatchedApps(patchedApps); await setPatchedApps(patchedApps);
} }
@@ -623,6 +645,37 @@ 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) {
@@ -690,8 +743,6 @@ class ManagerAPI {
Future<void> resetLastSelectedPatches() async { Future<void> resetLastSelectedPatches() async {
final File selectedPatchesFile = File(storedPatchesFile); final File selectedPatchesFile = File(storedPatchesFile);
if (selectedPatchesFile.existsSync()) { selectedPatchesFile.deleteSync();
selectedPatchesFile.deleteSync();
}
} }
} }

View File

@@ -25,13 +25,12 @@ 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;
@@ -46,23 +45,7 @@ class PatcherAPI {
} }
} }
List<String> getCompatiblePackages() { Future<void> _loadPatches() async {
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();
@@ -73,9 +56,6 @@ class PatcherAPI {
} }
_patches = List.empty(); _patches = List.empty();
} }
_compatiblePackages = getCompatiblePackages();
_universalPatches = getUniversalPatches();
} }
Future<List<ApplicationWithIcon>> getFilteredInstalledApps( Future<List<ApplicationWithIcon>> getFilteredInstalledApps(
@@ -83,31 +63,41 @@ class PatcherAPI {
) async { ) async {
final List<ApplicationWithIcon> filteredApps = []; final List<ApplicationWithIcon> filteredApps = [];
final bool allAppsIncluded = final bool allAppsIncluded =
_universalPatches.isNotEmpty && showUniversalPatches; _patches.any((patch) => patch.compatiblePackages.isEmpty) &&
showUniversalPatches;
if (allAppsIncluded) { if (allAppsIncluded) {
final appList = await DeviceApps.getInstalledApplications( final allPackages = await DeviceApps.getInstalledApplications(
includeAppIcons: true, includeAppIcons: true,
onlyAppsWithLaunchIntent: true, onlyAppsWithLaunchIntent: true,
); );
for (final pkg in allPackages) {
for (final app in appList) { if (!filteredApps.any((app) => app.packageName == pkg.packageName)) {
filteredApps.add(app as ApplicationWithIcon); final appInfo = await DeviceApps.getApp(
} pkg.packageName,
}
for (final packageName in _compatiblePackages) {
try {
if (!filteredApps.any((app) => app.packageName == packageName)) {
final ApplicationWithIcon? app = await DeviceApps.getApp(
packageName,
true, true,
) as ApplicationWithIcon?; ) as ApplicationWithIcon?;
if (app != null) { if (appInfo != null) {
filteredApps.add(app); filteredApps.add(appInfo);
} }
} }
} on Exception catch (e) { }
if (kDebugMode) { }
print(e); for (final Patch patch in _patches) {
for (final Package package in patch.compatiblePackages) {
try {
if (!filteredApps.any((app) => app.packageName == package.name)) {
final ApplicationWithIcon? app = await DeviceApps.getApp(
package.name,
true,
) as ApplicationWithIcon?;
if (app != null) {
filteredApps.add(app);
}
}
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
} }
} }
} }
@@ -115,10 +105,6 @@ class PatcherAPI {
} }
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) =>
@@ -146,20 +132,55 @@ 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;
@@ -167,11 +188,12 @@ 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,
@@ -197,7 +219,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();
@@ -205,11 +227,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) {
@@ -224,11 +246,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,
), ),
); );
@@ -242,12 +264,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) {
@@ -273,7 +295,7 @@ class PatcherAPI {
.replaceAll(':', '') .replaceAll(':', '')
.replaceAll('T', '') .replaceAll('T', '')
.replaceAll('.', ''); .replaceAll('.', '');
final String fileName = 'revanced-manager_patcher_$dateTime.txt'; final String fileName = 'revanced-manager_patcher_$dateTime.log';
final File log = File('${logDir.path}/$fileName'); final File log = File('${logDir.path}/$fileName');
log.writeAsStringSync(logs); log.writeAsStringSync(logs);
CRFileSaver.saveFileWithDialog( CRFileSaver.saveFileWithDialog(

View File

@@ -7,15 +7,12 @@ 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),
@@ -69,23 +66,21 @@ class RevancedAPI {
Future<Map<String, dynamic>?> _getLatestRelease( Future<Map<String, dynamic>?> _getLatestRelease(
String extension, String extension,
String repoName, String repoName,
) { ) async {
return getToolsLock.synchronized(() async { try {
try { final response = await _dio.get('/tools');
final response = await _dio.get('/tools'); final List<dynamic> tools = response.data['tools'];
final List<dynamic> tools = response.data['tools']; return tools.firstWhereOrNull(
return tools.firstWhereOrNull( (t) =>
(t) => t['repository'] == repoName &&
t['repository'] == repoName && (t['name'] as String).endsWith(extension),
(t['name'] as String).endsWith(extension), );
); } on Exception catch (e) {
} on Exception catch (e) { if (kDebugMode) {
if (kDebugMode) { print(e);
print(e);
}
return null;
} }
}); return null;
}
} }
Future<String?> getLatestReleaseVersion( Future<String?> getLatestReleaseVersion(

View File

@@ -1,16 +1,12 @@
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 StatefulWidget { class DynamicThemeBuilder extends StatelessWidget {
const DynamicThemeBuilder({ const DynamicThemeBuilder({
Key? key, Key? key,
required this.title, required this.title,
@@ -21,35 +17,6 @@ class DynamicThemeBuilder extends StatefulWidget {
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(
@@ -83,32 +50,24 @@ class _DynamicThemeBuilderState extends State<DynamicThemeBuilder> with WidgetsB
return DynamicTheme( return DynamicTheme(
themeCollection: ThemeCollection( themeCollection: ThemeCollection(
themes: { themes: {
0: brightness == Brightness.light ? lightCustomTheme : darkCustomTheme, 0: lightCustomTheme,
1: brightness == Brightness.light ? lightDynamicTheme : darkDynamicTheme, 1: darkCustomTheme,
2: lightCustomTheme, 2: lightDynamicTheme,
3: lightDynamicTheme, 3: darkDynamicTheme,
4: darkCustomTheme,
5: darkDynamicTheme,
}, },
fallbackTheme: PlatformDispatcher.instance.platformBrightness == Brightness.light ? lightCustomTheme : darkCustomTheme, fallbackTheme: lightCustomTheme,
), ),
builder: (context, theme) => MaterialApp( builder: (context, theme) => MaterialApp(
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
title: widget.title, title: title,
navigatorKey: StackedService.navigatorKey, navigatorKey: StackedService.navigatorKey,
onGenerateRoute: StackedRouter().onGenerateRoute, onGenerateRoute: StackedRouter().onGenerateRoute,
theme: theme, theme: theme,
home: widget.home, home: home,
localizationsDelegates: widget.localizationsDelegates, localizationsDelegates: localizationsDelegates,
), ),
); );
}, },
); );
} }
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
} }

View File

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

View File

@@ -73,6 +73,7 @@ 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,
@@ -201,6 +202,7 @@ 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,

View File

@@ -37,6 +37,7 @@ 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;
@@ -81,7 +82,7 @@ class HomeViewModel extends BaseViewModel {
_toast.showBottom('homeView.errorDownloadMessage'); _toast.showBottom('homeView.errorDownloadMessage');
} }
} }
_getPatchedApps();
_managerAPI.reAssessSavedApps().then((_) => _getPatchedApps()); _managerAPI.reAssessSavedApps().then((_) => _getPatchedApps());
} }
@@ -107,6 +108,10 @@ 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();
} }
@@ -464,7 +469,11 @@ class HomeViewModel extends BaseViewModel {
} }
Future<void> forceRefresh(BuildContext context) async { Future<void> forceRefresh(BuildContext context) async {
_managerAPI.clearAllData(); await Future.delayed(const Duration(seconds: 1));
if (_lastUpdate == null ||
_lastUpdate!.difference(DateTime.now()).inSeconds > 2) {
_managerAPI.clearAllData();
}
_toast.showBottom('homeView.refreshSuccess'); _toast.showBottom('homeView.refreshSuccess');
initialize(context); initialize(context);
} }

View File

@@ -15,8 +15,6 @@ import 'package:revanced_manager/services/root_api.dart';
import 'package:revanced_manager/services/toast.dart'; import 'package:revanced_manager/services/toast.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/about_info.dart';
import 'package:screenshot_callback/screenshot_callback.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
import 'package:wakelock/wakelock.dart'; import 'package:wakelock/wakelock.dart';
@@ -31,7 +29,6 @@ class InstallerViewModel extends BaseViewModel {
'app.revanced.manager.flutter/installer', 'app.revanced.manager.flutter/installer',
); );
final ScrollController scrollController = ScrollController(); final ScrollController scrollController = ScrollController();
final ScreenshotCallback screenshotCallback = ScreenshotCallback();
double? progress = 0.0; double? progress = 0.0;
String logs = ''; String logs = '';
String headerLogs = ''; String headerLogs = '';
@@ -41,7 +38,6 @@ class InstallerViewModel extends BaseViewModel {
bool hasErrors = false; bool hasErrors = false;
bool isCanceled = false; bool isCanceled = false;
bool cancel = false; bool cancel = false;
bool showPopupScreenshotWarning = true;
Future<void> initialize(BuildContext context) async { Future<void> initialize(BuildContext context) async {
isRooted = await _rootAPI.isRooted(); isRooted = await _rootAPI.isRooted();
@@ -68,12 +64,6 @@ class InstallerViewModel extends BaseViewModel {
} // ignore } // ignore
} }
} }
screenshotCallback.addListener(() {
if (showPopupScreenshotWarning) {
showPopupScreenshotWarning = false;
screenshotDetected(context);
}
});
await Wakelock.enable(); await Wakelock.enable();
await handlePlatformChannelMethods(); await handlePlatformChannelMethods();
await runPatcher(); await runPatcher();
@@ -140,28 +130,28 @@ class InstallerViewModel extends BaseViewModel {
Future<void> runPatcher() async { Future<void> runPatcher() async {
try { try {
await _patcherAPI.runPatcher( update(0.0, 'Initializing...', 'Initializing installer');
_app.packageName, if (_patches.isNotEmpty) {
_app.apkFilePath, try {
_patches, update(0.1, '', 'Creating working directory');
); await _patcherAPI.runPatcher(
} on Exception catch (e) { _app.packageName,
update( _app.apkFilePath,
-100.0, _patches,
'Failed...', );
'Something went wrong:\n$e', } on Exception catch (e) {
); update(
if (kDebugMode) { -100.0,
print(e); 'Aborted...',
'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();
@@ -179,72 +169,6 @@ class InstallerViewModel extends BaseViewModel {
} }
} }
Future<void> copyLogs() async {
final info = await AboutInfo.getInfo();
final formattedLogs = [
'```',
'~ Device Info',
'ReVanced Manager: ${info['version']}',
'Build: ${info['flavor']}',
'Model: ${info['model']}',
'Android version: ${info['androidVersion']}',
'Supported architectures: ${info['supportedArch'].join(", ")}',
'\n~ Patch Info',
'App: ${_app.packageName} v${_app.version}',
'Patches version: ${_managerAPI.patchesVersion}',
'Patches: ${_patches.map((p) => p.name).toList().join(", ")}',
'\n~ Settings',
'Enabled changing patches: ${_managerAPI.isPatchesChangeEnabled()}',
'Enabled universal patches: ${_managerAPI.areUniversalPatchesEnabled()}',
'Enabled experimental patches: ${_managerAPI.areExperimentalPatchesEnabled()}',
'Patches source: ${_managerAPI.getPatchesRepo()}',
'Integration source: ${_managerAPI.getIntegrationsRepo()}',
'\n~ Logs',
logs,
'```',
];
Clipboard.setData(ClipboardData(text: formattedLogs.join('\n')));
_toast.showBottom('installerView.copiedToClipboard');
}
Future<void> screenshotDetected(BuildContext context) async {
await showDialog(
context: context,
builder: (context) => AlertDialog(
title: I18nText(
'warning',
),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
icon: const Icon(Icons.warning),
content: SingleChildScrollView(
child: I18nText('installerView.screenshotDetected'),
),
actions: <Widget>[
CustomMaterialButton(
isFilled: false,
label: I18nText('noButton'),
onPressed: () {
Navigator.of(context).pop();
},
),
CustomMaterialButton(
label: I18nText('yesButton'),
onPressed: () {
copyLogs();
showPopupScreenshotWarning = true;
Navigator.of(context).pop();
},
),
],
),
);
}
Future<void> installTypeDialog(BuildContext context) async { Future<void> installTypeDialog(BuildContext context) async {
final ValueNotifier<int> installType = ValueNotifier(0); final ValueNotifier<int> installType = ValueNotifier(0);
if (isRooted) { if (isRooted) {
@@ -258,55 +182,52 @@ 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: SingleChildScrollView( content: ValueListenableBuilder(
child: ValueListenableBuilder( valueListenable: installType,
valueListenable: installType, builder: (context, value, child) {
builder: (context, value, child) { return Column(
return Column( mainAxisSize: MainAxisSize.min,
mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, children: [
children: [ Padding(
Padding( padding: const EdgeInsets.symmetric(
padding: const EdgeInsets.symmetric( horizontal: 20,
horizontal: 20, vertical: 10,
vertical: 10, ),
), child: I18nText(
child: I18nText( 'installerView.installTypeDescription',
'installerView.installTypeDescription', child: Text(
child: Text( '',
'', style: TextStyle(
style: TextStyle( fontSize: 16,
fontSize: 16, fontWeight: FontWeight.w500,
fontWeight: FontWeight.w500, color: Theme.of(context).colorScheme.secondary,
color: Theme.of(context).colorScheme.secondary,
),
), ),
), ),
), ),
RadioListTile( ),
title: I18nText('installerView.installNonRootType'), RadioListTile(
contentPadding: title: I18nText('installerView.installNonRootType'),
const EdgeInsets.symmetric(horizontal: 16), subtitle: I18nText('installerView.installRecommendedType'),
value: 0, contentPadding: const EdgeInsets.symmetric(horizontal: 10),
groupValue: value, value: 0,
onChanged: (selected) { groupValue: value,
installType.value = selected!; onChanged: (selected) {
}, installType.value = selected!;
), },
RadioListTile( ),
title: I18nText('installerView.installRootType'), RadioListTile(
contentPadding: title: I18nText('installerView.installRootType'),
const EdgeInsets.symmetric(horizontal: 16), contentPadding: const EdgeInsets.symmetric(horizontal: 10),
value: 1, value: 1,
groupValue: value, groupValue: value,
onChanged: (selected) { onChanged: (selected) {
installType.value = selected!; installType.value = selected!;
}, },
), ),
], ],
); );
}, },
),
), ),
actions: [ actions: [
CustomMaterialButton( CustomMaterialButton(
@@ -334,9 +255,9 @@ class InstallerViewModel extends BaseViewModel {
Future<void> stopPatcher() async { Future<void> stopPatcher() async {
try { try {
isCanceled = true; isCanceled = true;
update(0.5, 'Canceling...', 'Canceling patching process'); update(0.5, 'Aborting...', 'Canceling patching process');
await _patcherAPI.stopPatcher(); await _patcherAPI.stopPatcher();
update(-100.0, 'Canceled...', 'Press back to exit'); update(-100.0, 'Aborted...', 'Press back to exit');
} on Exception catch (e) { } on Exception catch (e) {
if (kDebugMode) { if (kDebugMode) {
print(e); print(e);
@@ -347,33 +268,56 @@ 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;
update( final bool hasMicroG =
1.0, _patches.any((p) => p.name.endsWith('MicroG support'));
'Installing...', final bool rootMicroG = installAsRoot && hasMicroG;
_app.isRooted final bool rootFromStorage = installAsRoot && _app.isFromStorage;
? 'Installing patched file using root method' final bool ytWithoutRootMicroG =
: 'Installing patched file using nonroot method', !installAsRoot && !hasMicroG && _app.packageName.contains('youtube');
); if (rootMicroG || rootFromStorage || ytWithoutRootMicroG) {
isInstalled = await _patcherAPI.installPatchedFile(_app); return showDialog(
if (isInstalled) { context: context,
_app.isFromStorage = false; builder: (context) => AlertDialog(
_app.patchDate = DateTime.now(); title: I18nText('installerView.installErrorDialogTitle'),
_app.appliedPatches = _patches.map((p) => p.name).toList(); backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
content: I18nText(
// In case a patch changed the app name or package name, rootMicroG
// update the app info. ? 'installerView.installErrorDialogText1'
final app = : rootFromStorage
await DeviceApps.getAppFromStorage(_patcherAPI.outFile!.path); ? 'installerView.installErrorDialogText3'
if (app != null) { : 'installerView.installErrorDialogText2',
_app.name = app.appName; ),
_app.packageName = app.packageName; actions: <Widget>[
} CustomMaterialButton(
label: I18nText('okButton'),
await _managerAPI.savePatchedApp(_app); onPressed: () => Navigator.of(context).pop(),
),
update(1.0, 'Installed!', 'Installed!'); ],
),
);
} else { } else {
// TODO(aabed): Show error message. 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);
}
} }
} on Exception catch (e) { } on Exception catch (e) {
if (kDebugMode) { if (kDebugMode) {
@@ -392,6 +336,10 @@ class InstallerViewModel extends BaseViewModel {
} }
} }
void exportLog() {
_patcherAPI.exportPatcherLog(logs);
}
Future<void> cleanPatcher() async { Future<void> cleanPatcher() async {
try { try {
_patcherAPI.cleanPatcher(); _patcherAPI.cleanPatcher();
@@ -415,7 +363,7 @@ class InstallerViewModel extends BaseViewModel {
exportResult(); exportResult();
break; break;
case 1: case 1:
copyLogs(); exportLog();
break; break;
} }
} }
@@ -437,7 +385,6 @@ class InstallerViewModel extends BaseViewModel {
} else { } else {
_patcherAPI.cleanPatcher(); _patcherAPI.cleanPatcher();
} }
screenshotCallback.dispose();
Navigator.of(context).pop(); Navigator.of(context).pop();
return true; return true;
} }

View File

@@ -1,5 +1,4 @@
// 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';
@@ -31,23 +30,12 @@ class NavigationViewModel extends IndexTrackingViewModel {
); );
} }
final dynamicTheme = DynamicTheme.of(context)!; if (prefs.getBool('useDarkTheme') == null) {
if (prefs.getInt('themeMode') == null) { final bool isDark =
await prefs.setInt('themeMode', 0); MediaQuery.platformBrightnessOf(context) != Brightness.light;
await dynamicTheme.setTheme(0); await prefs.setBool('useDarkTheme', isDark);
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(

View File

@@ -44,6 +44,49 @@ 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(
@@ -72,7 +115,7 @@ class PatcherViewModel extends BaseViewModel {
), ),
); );
} else { } else {
showArmv7WarningDialog(context); // TODO(aabed): Find out why this is here showArmv7WarningDialog(context);
} }
} }
@@ -142,9 +185,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!.packageName); await _managerAPI.getSelectedPatches(selectedApp!.originalPackageName);
final List<Patch> patches = final List<Patch> patches =
_patcherAPI.getFilteredPatches(selectedApp!.packageName); _patcherAPI.getFilteredPatches(selectedApp!.originalPackageName);
this this
.selectedPatches .selectedPatches
.addAll(patches.where((patch) => selectedPatches.contains(patch.name))); .addAll(patches.where((patch) => selectedPatches.contains(patch.name)));
@@ -160,7 +203,7 @@ class PatcherViewModel extends BaseViewModel {
.selectedPatches .selectedPatches
.removeWhere((patch) => patch.compatiblePackages.isEmpty); .removeWhere((patch) => patch.compatiblePackages.isEmpty);
} }
final usedPatches = _managerAPI.getUsedPatches(selectedApp!.packageName); final usedPatches = _managerAPI.getUsedPatches(selectedApp!.originalPackageName);
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}');

View File

@@ -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(66.0), preferredSize: const Size.fromHeight(64.0),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
vertical: 8.0, vertical: 8.0,
@@ -194,7 +194,7 @@ 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),
@@ -246,7 +246,7 @@ 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:

View File

@@ -28,7 +28,7 @@ class PatchesSelectorViewModel extends BaseViewModel {
getPatchesVersion().whenComplete(() => notifyListeners()); getPatchesVersion().whenComplete(() => notifyListeners());
patches.addAll( patches.addAll(
_patcherAPI.getFilteredPatches( _patcherAPI.getFilteredPatches(
selectedApp!.packageName, selectedApp!.originalPackageName,
), ),
); );
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?.packageName != null) { if (locator<PatcherViewModel>().selectedApp?.originalPackageName != null) {
selectedPatches.addAll( selectedPatches.addAll(
_patcherAPI _patcherAPI
.getFilteredPatches( .getFilteredPatches(
locator<PatcherViewModel>().selectedApp!.packageName, locator<PatcherViewModel>().selectedApp!.originalPackageName,
) )
.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!.packageName, locator<PatcherViewModel>().selectedApp!.originalPackageName,
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!.packageName, locator<PatcherViewModel>().selectedApp!.originalPackageName,
); );
if (selectedPatches.isNotEmpty) { if (selectedPatches.isNotEmpty) {
this.selectedPatches.clear(); this.selectedPatches.clear();

View File

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

View File

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

View File

@@ -222,6 +222,22 @@ 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),

View File

@@ -13,6 +13,7 @@ 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 {
@@ -146,7 +147,17 @@ class AppInfoViewModel extends BaseViewModel {
} }
String getAppliedPatchesString(List<String> appliedPatches) { String getAppliedPatchesString(List<String> appliedPatches) {
return '\u2022 ${appliedPatches.join('\n\u2022 ')}'; final List<String> names = appliedPatches
.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) {

View File

@@ -79,6 +79,8 @@ 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),
), ),

View File

@@ -5,8 +5,8 @@ import 'package:flutter_i18n/widgets/I18nText.dart';
import 'package:revanced_manager/ui/views/settings/settingsFragment/settings_manage_api_url.dart'; import 'package:revanced_manager/ui/views/settings/settingsFragment/settings_manage_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_auto_update_patches.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_enable_patches_selection.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_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';

View File

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

View File

@@ -1,5 +1,6 @@
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';
@@ -12,84 +13,151 @@ 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(
child: Row( onTap: () {
mainAxisAlignment: MainAxisAlignment.spaceBetween, expController.toggle();
children: [ _animationController.isCompleted
Flexible( ? _animationController.reverse()
child: Row( : _animationController.forward();
children: [ },
SizedBox( child: ExpandablePanel(
width: 40, controller: expController,
child: Image.memory(widget.icon, height: 40, width: 40), theme: const ExpandableThemeData(
), inkWellBorderRadius: BorderRadius.all(Radius.circular(16)),
const SizedBox(width: 19), tapBodyToCollapse: false,
Expanded( tapBodyToExpand: false,
child: Column( tapHeaderToExpand: false,
crossAxisAlignment: CrossAxisAlignment.start, hasIcon: false,
children: <Widget>[ animationDuration: Duration(milliseconds: 450),
Text( ),
widget.name, header: Row(
maxLines: 1, mainAxisAlignment: MainAxisAlignment.spaceBetween,
overflow: TextOverflow.ellipsis, children: [
style: const TextStyle( Flexible(
fontSize: 16, child: Row(
fontWeight: FontWeight.w500, children: [
), SizedBox(
), width: 40,
Text( child: Image.memory(widget.icon, height: 40, width: 40),
format(widget.patchDate),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
],
), ),
), const SizedBox(width: 19),
], Expanded(
), child: Column(
), crossAxisAlignment: CrossAxisAlignment.start,
Row( children: <Widget>[
children: [ Text(
const SizedBox(width: 8), widget.name,
Column( maxLines: 1,
mainAxisAlignment: MainAxisAlignment.center, overflow: TextOverflow.ellipsis,
crossAxisAlignment: CrossAxisAlignment.end, style: const TextStyle(
children: <Widget>[ fontSize: 16,
CustomMaterialButton( fontWeight: FontWeight.w500,
label: I18nText('applicationItem.infoButton'), ),
onPressed: widget.onPressed, ),
Text(
format(widget.patchDate),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
],
),
), ),
], ],
), ),
),
Row(
children: [
RotationTransition(
turns: Tween(begin: 0.0, end: 0.50)
.animate(_animationController),
child: const Padding(
padding: EdgeInsets.all(8.0),
child: Icon(Icons.arrow_drop_down),
),
),
const SizedBox(width: 8),
Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
CustomMaterialButton(
label: widget.isUpdatableApp
? I18nText('applicationItem.patchButton')
: I18nText('applicationItem.infoButton'),
onPressed: widget.onPressed,
),
],
),
],
),
],
),
collapsed: const SizedBox(),
expanded: Padding(
padding: const EdgeInsets.only(
top: 16.0,
left: 4.0,
right: 4.0,
bottom: 4.0,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
I18nText(
'applicationItem.changelogLabel',
child: const Text(
'',
style: TextStyle(fontWeight: FontWeight.w700),
),
),
const SizedBox(height: 4),
Text('\u2022 ${widget.changelog.join('\n\u2022 ')}'),
], ],
), ),
], ),
), ),
), ),
); );

View File

@@ -4,7 +4,7 @@ homepage: https://github.com/revanced/revanced-manager
publish_to: 'none' publish_to: 'none'
version: 1.11.2+101100200 version: 1.9.5+100900500
environment: environment:
sdk: '>=3.0.0 <4.0.0' sdk: '>=3.0.0 <4.0.0'
@@ -75,8 +75,6 @@ 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
screenshot_callback: ^3.0.1
synchronized: ^3.1.0
dev_dependencies: dev_dependencies:
json_serializable: ^6.6.1 json_serializable: ^6.6.1