feat: patcher

Co-authored-by: Canny <canny1913@outlook.com>
This commit is contained in:
Ushie 2022-09-15 00:55:15 +03:00
parent f41064a75c
commit ec91ee8ae3
No known key found for this signature in database
GPG Key ID: 0EF73F1CA38B2D5F
82 changed files with 2382 additions and 460 deletions

11
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,11 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "hourly"

34
.github/workflows/android.yml vendored Normal file
View File

@ -0,0 +1,34 @@
name: Android CI
on:
push:
branches: [ "dev" ]
pull_request:
branches: [ "dev" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: set up JDK 11
uses: actions/setup-java@v3
with:
java-version: '11'
distribution: 'temurin'
cache: gradle
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Build with Gradle
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: ./gradlew assembleDebug
- name: Upload APK
uses: actions/upload-artifact@v2
with:
name: manager
path: ./app/build/outputs/apk/debug/app-debug.apk

34
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,34 @@
name: Android Release
on:
push:
branches: [ "dev" ]
pull_request:
branches: [ "dev" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: set up JDK 11
uses: actions/setup-java@v3
with:
java-version: '11'
distribution: 'temurin'
cache: gradle
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Build with Gradle
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: ./gradlew assembleRelease
- name: Upload APK
uses: actions/upload-artifact@v2
with:
name: manager
path: ./app/build/outputs/apk/release/app-release-unsigned.apk

View File

@ -1,18 +1,37 @@
plugins {
id("com.android.application")
id("kotlin-android")
id("org.jetbrains.kotlin.android")
id("com.google.devtools.ksp")
id("kotlin-parcelize")
kotlin("plugin.serialization") version "1.7.10"
}
repositories {
mavenCentral()
maven("https://jitpack.io")
google()
maven {
url = uri("https://maven.pkg.github.com/revanced/revanced-patcher")
credentials {
username = (project.findProperty("gpr.user") ?: System.getenv("GITHUB_ACTOR")) as String
password = (project.findProperty("gpr.key") ?: System.getenv("GITHUB_TOKEN")) as String
}
}
}
android {
namespace = "app.revanced.manager"
compileSdk = 32
compileSdk = 33
lint {
abortOnError = false
disable += "DialogFragmentCallbacksDetector"
}
defaultConfig {
applicationId = "app.revanced.manager.compose"
minSdk = 26
targetSdk = 32
targetSdk = 33
versionCode = 1
versionName = "0.0.1"
@ -22,7 +41,11 @@ android {
buildTypes {
release {
isMinifyEnabled = true
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
@ -36,17 +59,28 @@ android {
freeCompilerArgs = freeCompilerArgs + "-opt-in=kotlin.RequiresOptIn"
}
packagingOptions {
resources {
excludes += "/prebuilt/**"
excludes += "/**/*.version"
excludes += "/kotlin-tooling-metadata.json"
excludes += "/okhttp3/**"
excludes += "/DebugProbesKt.bin"
}
}
buildFeatures.compose = true
composeOptions.kotlinCompilerExtensionVersion = "1.2.0"
composeOptions.kotlinCompilerExtensionVersion = "1.3.0-rc02"
}
dependencies {
// AndroidX core
implementation("androidx.core:core-ktx:1.8.0")
implementation("androidx.core:core-ktx:1.9.0")
implementation("androidx.core:core-splashscreen:1.0.0")
// AndroidX activity
implementation("androidx.activity:activity-compose:1.6.0-alpha05")
implementation("androidx.activity:activity-compose:1.6.0-rc02")
implementation("androidx.work:work-runtime-ktx:2.7.1")
// Koin
val koinVersion = "3.2.0"
@ -54,16 +88,17 @@ dependencies {
implementation("io.insert-koin:koin-androidx-compose:$koinVersion")
// Compose
val composeVersion = "1.3.0-alpha01"
val composeVersion = "1.3.0-alpha03"
implementation("androidx.compose.ui:ui:${composeVersion}")
debugImplementation("androidx.compose.ui:ui-tooling:${composeVersion}")
implementation("androidx.compose.material3:material3:1.0.0-alpha15")
implementation("androidx.compose.material3:material3:1.0.0-beta02")
implementation("androidx.compose.material:material-icons-extended:${composeVersion}")
// Accompanist
val accompanistVersion = "0.26.0-alpha"
implementation("com.google.accompanist:accompanist-systemuicontroller:$accompanistVersion")
implementation("com.google.accompanist:accompanist-placeholder-material:$accompanistVersion")
implementation("com.google.accompanist:accompanist-drawablepainter:$accompanistVersion")
// Coil (async image loading)
implementation("io.coil-kt:coil-compose:2.1.0")
@ -74,10 +109,20 @@ dependencies {
// Taxi (navigation)
implementation("com.github.X1nto:Taxi:1.2.0")
// Ktor
val ktorVersion = "2.0.3"
implementation("io.ktor:ktor-client-core:$ktorVersion")
implementation("io.ktor:ktor-client-android:$ktorVersion")
implementation("io.ktor:ktor-client-cio:$ktorVersion")
implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion")
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
// ReVanced
implementation("app.revanced:revanced-patcher:4.2.3")
// Coil for network image
implementation("io.coil-kt:coil-compose:2.1.0")
// Signing & aligning
implementation("org.bouncycastle:bcpkix-jdk15on:1.70")
implementation("com.android.tools.build:apksig:7.2.2")
}

View File

@ -14,8 +14,41 @@
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
-keepattributes SourceFile,LineNumberTable
-dontobfuscate
-if @kotlinx.serialization.Serializable class **
-keepclassmembers class <1> {
static <1>$Companion Companion;
}
-if @kotlinx.serialization.Serializable class ** {
static **$* *;
}
-keepclassmembers class <2>$<3> {
kotlinx.serialization.KSerializer serializer(...);
}
-if @kotlinx.serialization.Serializable class ** {
public static ** INSTANCE;
}
-keepclassmembers class <1> {
public static <1> INSTANCE;
kotlinx.serialization.KSerializer serializer(...);
}
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
# These classes required for the patcher to function correctly.
-keep class app.revanced.patcher.** {
*;
}
-keep class brut.** {
*;
}
-keep class org.xmlpull.** {
*;
}
-keep class kotlin.** {
*;
}
-keep class org.jf.** {
*;
}
-keepattributes RuntimeVisibleAnnotations,AnnotationDefault

View File

@ -2,26 +2,46 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<permission
android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="ReservedSystemPermission" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission
android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<queries>
<intent>
<action android:name="android.intent.action.MAIN" />
</intent>
</queries>
<application
android:name=".ManagerApplication"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:extractNativeLibs="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:largeHeap="true"
android:theme="@style/Theme.ReVancedManager"
tools:targetApi="31">
tools:targetApi="33">
<activity
android:name=".MainActivity"
android:exported="true"
android:theme="@style/Theme.ReVancedManager">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@ -8,12 +8,17 @@ import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.with
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.ui.Modifier
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import app.revanced.manager.preferences.PreferencesManager
import app.revanced.manager.ui.navigation.AppDestination
import app.revanced.manager.ui.screen.MainDashboardScreen
import app.revanced.manager.ui.screen.subscreens.AppSelectorSubscreen
import app.revanced.manager.ui.screen.subscreens.PatchesSelectorSubscreen
import app.revanced.manager.ui.theme.ReVancedManagerTheme
import app.revanced.manager.ui.theme.Theme
import com.xinto.taxi.Taxi
import com.xinto.taxi.rememberBackstackNavigator
import org.koin.android.ext.android.inject
@ -23,9 +28,13 @@ class MainActivity : ComponentActivity() {
@OptIn(ExperimentalAnimationApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen()
super.onCreate(savedInstanceState)
setContent {
ReVancedManagerTheme(dynamicColor = prefs.dynamicColor) {
ReVancedManagerTheme(
dynamicColor = prefs.dynamicColor,
darkTheme = prefs.theme == Theme.SYSTEM && isSystemInDarkTheme() || prefs.theme == Theme.DARK,
) {
val navigator = rememberBackstackNavigator<AppDestination>(AppDestination.Dashboard)
BackHandler {
@ -39,6 +48,10 @@ class MainActivity : ComponentActivity() {
) { destination ->
when (destination) {
is AppDestination.Dashboard -> MainDashboardScreen(navigator = navigator)
is AppDestination.AppSelector -> AppSelectorSubscreen(
navigator = navigator
)
is AppDestination.PatchSelector -> PatchesSelectorSubscreen(navigator = navigator)
}
}
}

View File

@ -0,0 +1,19 @@
package app.revanced.manager
import android.content.pm.ApplicationInfo
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import app.revanced.manager.ui.Resource
import app.revanced.patcher.data.Data
import app.revanced.patcher.patch.Patch
import java.util.*
object Variables {
val selectedAppPackage = mutableStateOf(Optional.empty<String>())
val selectedPatches = mutableStateListOf<String>()
val patches = mutableStateOf<Resource<List<Class<out Patch<Data>>>>>(Resource.Loading)
val patchesState by patches
val filteredApps = mutableListOf<ApplicationInfo>()
}

View File

@ -0,0 +1,90 @@
package app.revanced.manager.api
import android.util.Log
import app.revanced.manager.dto.github.APIRelease
import app.revanced.manager.preferences.PreferencesManager
import app.revanced.manager.repository.GitHubRepository
import io.ktor.client.*
import io.ktor.client.engine.android.*
import io.ktor.client.plugins.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.util.cio.*
import io.ktor.utils.io.*
import kotlinx.serialization.json.Json
import java.io.File
val client = HttpClient(Android) {
BrowserUserAgent()
install(ContentNegotiation) {
json(Json {
encodeDefaults = true
isLenient = true
ignoreUnknownKeys = true
})
}
}
class API(private val repository: GitHubRepository, private val prefs: PreferencesManager) {
private suspend fun findAsset(repo: String, file: String): PatchesAsset {
val release = repository.getLatestRelease(repo)
val asset = release.assets.findAsset(file) ?: throw MissingAssetException()
return PatchesAsset(release, asset)
}
private fun List<APIRelease.Asset>.findAsset(file: String) = find { asset ->
(asset.name.contains(file) && !asset.name.contains("-sources") && !asset.name.contains("-javadoc"))
}
suspend fun downloadPatchBundle(workdir: File): File {
return try {
val (_, out) = downloadAsset(workdir, findAsset(prefs.srcPatches.toString(), ".jar"))
out
} catch (e: Exception) {
throw Exception("Failed to download patch bundle", e)
}
}
suspend fun downloadIntegrations(workdir: File): File {
return try {
val (_, out) = downloadAsset(
workdir,
findAsset(prefs.srcIntegrations.toString(), ".apk")
)
out
} catch (e: Exception) {
throw Exception("Failed to download integrations", e)
}
}
private suspend fun downloadAsset(
workdir: File,
patchesAsset: PatchesAsset
): Pair<PatchesAsset, File> {
val (release, asset) = patchesAsset
val out = workdir.resolve("${release.tagName}-${asset.name}")
if (out.exists()) {
Log.d(
"ReVanced Manager",
"Skipping downloading asset ${asset.name} because it exists in cache!"
)
return patchesAsset to out
}
Log.d("ReVanced Manager", "Downloading asset ${asset.name}")
client.get(asset.downloadUrl)
.bodyAsChannel()
.copyAndClose(out.writeChannel())
return patchesAsset to out
}
}
data class PatchesAsset(
val release: APIRelease,
val asset: APIRelease.Asset
)
class MissingAssetException : Exception()

View File

@ -1,9 +1,11 @@
package app.revanced.manager.di
import app.revanced.manager.api.API
import app.revanced.manager.repository.GitHubRepository
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module
val repositoryModule = module {
singleOf(::GitHubRepository)
singleOf(::API)
}

View File

@ -1,10 +1,15 @@
package app.revanced.manager.di
import app.revanced.manager.ui.viewmodel.*
import app.revanced.manager.ui.viewmodel.AppSelectorViewModel
import app.revanced.manager.ui.viewmodel.DashboardViewModel
import app.revanced.manager.ui.viewmodel.PatcherViewModel
import app.revanced.manager.ui.viewmodel.SettingsViewModel
import org.koin.androidx.viewmodel.dsl.viewModelOf
import org.koin.dsl.module
val viewModelModule = module {
viewModelOf(::SettingsViewModel)
viewModelOf(::DashboardViewModel)
viewModelOf(::PatcherViewModel)
viewModelOf(::AppSelectorViewModel)
}

View File

@ -3,7 +3,7 @@ package app.revanced.manager.dto.github
import kotlinx.serialization.Serializable
@Serializable
class ApiCommit(
class APICommit(
val sha: String,
val commit: Object
) {

View File

@ -4,7 +4,7 @@ import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
class ApiContributor(
class APIContributor(
@SerialName("login") val username: String,
@SerialName("avatar_url") val avatarUrl: String,
@SerialName("html_url") val profileUrl: String,

View File

@ -4,7 +4,7 @@ import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class ApiRelease(
data class APIRelease(
@SerialName("tag_name") val tagName: String,
@SerialName("published_at") val publishedAt: String,
val prerelease: Boolean,

View File

@ -0,0 +1,12 @@
package app.revanced.manager.patcher.aapt
import android.content.Context
import java.io.File
object Aapt {
fun binary(context: Context): File {
return File(context.applicationInfo.nativeLibraryDir).resolveAapt()
}
}
private fun File.resolveAapt() = resolve(list { _, f -> !File(f).isDirectory }!!.first())

View File

@ -0,0 +1,11 @@
package app.revanced.manager.patcher.aligning
import app.revanced.manager.patcher.aligning.zip.structures.ZipEntry
internal object ZipAligner {
private const val DEFAULT_ALIGNMENT = 4
private const val LIBRARY_ALIGNMENT = 4096
fun getEntryAlignment(entry: ZipEntry): Int? =
if (entry.compression.toUInt() != 0u) null else if (entry.fileName.endsWith(".so")) LIBRARY_ALIGNMENT else DEFAULT_ALIGNMENT
}

View File

@ -0,0 +1,33 @@
package app.revanced.manager.patcher.aligning.zip
import java.io.DataInput
import java.io.DataOutput
import java.nio.ByteBuffer
fun UInt.toLittleEndian() =
(((this.toInt() and 0xff000000.toInt()) shr 24) or ((this.toInt() and 0x00ff0000) shr 8) or ((this.toInt() and 0x0000ff00) shl 8) or (this.toInt() shl 24)).toUInt()
fun UShort.toLittleEndian() = (this.toUInt() shl 16).toLittleEndian().toUShort()
fun UInt.toBigEndian() = (((this.toInt() and 0xff) shl 24) or ((this.toInt() and 0xff00) shl 8)
or ((this.toInt() and 0x00ff0000) ushr 8) or (this.toInt() ushr 24)).toUInt()
fun UShort.toBigEndian() = (this.toUInt() shl 16).toBigEndian().toUShort()
fun ByteBuffer.getUShort() = this.getShort().toUShort()
fun ByteBuffer.getUInt() = this.getInt().toUInt()
fun ByteBuffer.putUShort(ushort: UShort) = this.putShort(ushort.toShort())
fun ByteBuffer.putUInt(uint: UInt) = this.putInt(uint.toInt())
fun DataInput.readUShort() = this.readShort().toUShort()
fun DataInput.readUInt() = this.readInt().toUInt()
fun DataOutput.writeUShort(ushort: UShort) = this.writeShort(ushort.toInt())
fun DataOutput.writeUInt(uint: UInt) = this.writeInt(uint.toInt())
fun DataInput.readUShortLE() = this.readUShort().toBigEndian()
fun DataInput.readUIntLE() = this.readUInt().toBigEndian()
fun DataOutput.writeUShortLE(ushort: UShort) = this.writeUShort(ushort.toLittleEndian())
fun DataOutput.writeUIntLE(uint: UInt) = this.writeUInt(uint.toLittleEndian())

View File

@ -0,0 +1,176 @@
package app.revanced.manager.patcher.aligning.zip
import app.revanced.manager.patcher.aligning.zip.structures.ZipEndRecord
import app.revanced.manager.patcher.aligning.zip.structures.ZipEntry
import java.io.Closeable
import java.io.File
import java.io.RandomAccessFile
import java.nio.ByteBuffer
import java.nio.channels.FileChannel
import java.util.zip.CRC32
import java.util.zip.Deflater
class ZipFile(val file: File) : Closeable {
var entries: MutableList<ZipEntry> = mutableListOf()
private val filePointer: RandomAccessFile = RandomAccessFile(file, "rw")
private var CDNeedsRewrite = false
private val compressionLevel = 5
init {
//if file isn't empty try to load entries
if (file.length() > 0) {
val endRecord = findEndRecord()
if (endRecord.diskNumber > 0u || endRecord.totalEntries != endRecord.diskEntries)
throw IllegalArgumentException("Multi-file archives are not supported")
entries = readEntries(endRecord).toMutableList()
}
//seek back to start for writing
filePointer.seek(0)
}
private fun findEndRecord(): ZipEndRecord {
//look from end to start since end record is at the end
for (i in filePointer.length() - 1 downTo 0) {
filePointer.seek(i)
//possible beginning of signature
if (filePointer.readByte() == 0x50.toByte()) {
//seek back to get the full int
filePointer.seek(i)
val possibleSignature = filePointer.readUIntLE()
if (possibleSignature == ZipEndRecord.ECD_SIGNATURE) {
filePointer.seek(i)
return ZipEndRecord.fromECD(filePointer)
}
}
}
throw Exception("Couldn't find end record")
}
private fun readEntries(endRecord: ZipEndRecord): List<ZipEntry> {
filePointer.seek(endRecord.centralDirectoryStartOffset.toLong())
val numberOfEntries = endRecord.diskEntries.toInt()
return buildList(numberOfEntries) {
for (i in 1..numberOfEntries) {
add(
ZipEntry.fromCDE(filePointer).also
{
//for some reason the local extra field can be different from the central one
it.readLocalExtra(
filePointer.channel.map(
FileChannel.MapMode.READ_ONLY,
it.localHeaderOffset.toLong() + 28,
2
)
)
})
}
}
}
private fun writeCD() {
val CDStart = filePointer.channel.position().toUInt()
entries.forEach {
filePointer.channel.write(it.toCDE())
}
val entriesCount = entries.size.toUShort()
val endRecord = ZipEndRecord(
0u,
0u,
entriesCount,
entriesCount,
filePointer.channel.position().toUInt() - CDStart,
CDStart,
""
)
filePointer.channel.write(endRecord.toECD())
}
private fun addEntry(entry: ZipEntry, data: ByteBuffer) {
CDNeedsRewrite = true
entry.localHeaderOffset = filePointer.channel.position().toUInt()
filePointer.channel.write(entry.toLFH())
filePointer.channel.write(data)
entries.add(entry)
}
fun addEntryCompressData(entry: ZipEntry, data: ByteArray) {
val compressor = Deflater(compressionLevel, true)
compressor.setInput(data)
compressor.finish()
val uncompressedSize = data.size
val compressedData =
ByteArray(uncompressedSize) //i'm guessing compression won't make the data bigger
val compressedDataLength = compressor.deflate(compressedData)
val compressedBuffer =
ByteBuffer.wrap(compressedData.take(compressedDataLength).toByteArray())
compressor.end()
val crc = CRC32()
crc.update(data)
entry.compression = 8u //deflate compression
entry.uncompressedSize = uncompressedSize.toUInt()
entry.compressedSize = compressedDataLength.toUInt()
entry.crc32 = crc.value.toUInt()
addEntry(entry, compressedBuffer)
}
fun addEntryCopyData(entry: ZipEntry, data: ByteBuffer, alignment: Int? = null) {
alignment?.let { alignment ->
//calculate where data would end up
val dataOffset = filePointer.filePointer + entry.LFHSize
val mod = dataOffset % alignment
//wrong alignment
if (mod != 0L) {
//add padding at end of extra field
entry.localExtraField =
entry.localExtraField.copyOf((entry.localExtraField.size + (alignment - mod)).toInt())
}
}
addEntry(entry, data)
}
fun getDataForEntry(entry: ZipEntry): ByteBuffer {
return filePointer.channel.map(
FileChannel.MapMode.READ_ONLY,
entry.dataOffset.toLong(),
entry.compressedSize.toLong()
)
}
fun copyEntriesFromFileAligned(file: ZipFile, entryAlignment: (entry: ZipEntry) -> Int?) {
for (entry in file.entries) {
if (entries.any { it.fileName == entry.fileName }) continue //don't add duplicates
val data = file.getDataForEntry(entry)
addEntryCopyData(entry, data, entryAlignment(entry))
}
}
override fun close() {
if (CDNeedsRewrite) writeCD()
filePointer.close()
}
}

View File

@ -0,0 +1,78 @@
package app.revanced.manager.patcher.aligning.zip.structures
import app.revanced.manager.patcher.aligning.zip.putUInt
import app.revanced.manager.patcher.aligning.zip.putUShort
import app.revanced.manager.patcher.aligning.zip.readUIntLE
import app.revanced.manager.patcher.aligning.zip.readUShortLE
import java.io.DataInput
import java.nio.ByteBuffer
import java.nio.ByteOrder
data class ZipEndRecord(
val diskNumber: UShort,
val startingDiskNumber: UShort,
val diskEntries: UShort,
val totalEntries: UShort,
val centralDirectorySize: UInt,
val centralDirectoryStartOffset: UInt,
val fileComment: String,
) {
companion object {
const val ECD_HEADER_SIZE = 22
const val ECD_SIGNATURE = 0x06054b50u
fun fromECD(input: DataInput): ZipEndRecord {
val signature = input.readUIntLE()
if (signature != ECD_SIGNATURE)
throw IllegalArgumentException("Input doesn't start with end record signature")
val diskNumber = input.readUShortLE()
val startingDiskNumber = input.readUShortLE()
val diskEntries = input.readUShortLE()
val totalEntries = input.readUShortLE()
val centralDirectorySize = input.readUIntLE()
val centralDirectoryStartOffset = input.readUIntLE()
val fileCommentLength = input.readUShortLE()
var fileComment = ""
if (fileCommentLength > 0u) {
val fileCommentBytes = ByteArray(fileCommentLength.toInt())
input.readFully(fileCommentBytes)
fileComment = fileCommentBytes.toString(Charsets.UTF_8)
}
return ZipEndRecord(
diskNumber,
startingDiskNumber,
diskEntries,
totalEntries,
centralDirectorySize,
centralDirectoryStartOffset,
fileComment
)
}
}
fun toECD(): ByteBuffer {
val commentBytes = fileComment.toByteArray(Charsets.UTF_8)
val buffer = ByteBuffer.allocate(ECD_HEADER_SIZE + commentBytes.size)
.also { it.order(ByteOrder.LITTLE_ENDIAN) }
buffer.putUInt(ECD_SIGNATURE)
buffer.putUShort(diskNumber)
buffer.putUShort(startingDiskNumber)
buffer.putUShort(diskEntries)
buffer.putUShort(totalEntries)
buffer.putUInt(centralDirectorySize)
buffer.putUInt(centralDirectoryStartOffset)
buffer.putUShort(commentBytes.size.toUShort())
buffer.put(commentBytes)
buffer.flip()
return buffer
}
}

View File

@ -0,0 +1,189 @@
package app.revanced.manager.patcher.aligning.zip.structures
import app.revanced.manager.patcher.aligning.zip.*
import java.io.DataInput
import java.nio.ByteBuffer
import java.nio.ByteOrder
data class ZipEntry(
val version: UShort,
val versionNeeded: UShort,
val flags: UShort,
var compression: UShort,
val modificationTime: UShort,
val modificationDate: UShort,
var crc32: UInt,
var compressedSize: UInt,
var uncompressedSize: UInt,
val diskNumber: UShort,
val internalAttributes: UShort,
val externalAttributes: UInt,
var localHeaderOffset: UInt,
val fileName: String,
val extraField: ByteArray,
val fileComment: String,
var localExtraField: ByteArray = ByteArray(0), //separate for alignment
) {
val LFHSize: Int
get() = LFH_HEADER_SIZE + fileName.toByteArray(Charsets.UTF_8).size + localExtraField.size
val dataOffset: UInt
get() = localHeaderOffset + LFHSize.toUInt()
companion object {
const val CDE_HEADER_SIZE = 46
const val CDE_SIGNATURE = 0x02014b50u
const val LFH_HEADER_SIZE = 30
const val LFH_SIGNATURE = 0x04034b50u
fun createWithName(fileName: String): ZipEntry {
return ZipEntry(
0x1403u, //made by unix, version 20
0u,
0u,
0u,
0x0821u, //seems to be static time google uses, no idea
0x0221u, //same as above
0u,
0u,
0u,
0u,
0u,
0u,
0u,
fileName,
ByteArray(0),
""
)
}
fun fromCDE(input: DataInput): ZipEntry {
val signature = input.readUIntLE()
if (signature != CDE_SIGNATURE)
throw IllegalArgumentException("Input doesn't start with central directory entry signature")
val version = input.readUShortLE()
val versionNeeded = input.readUShortLE()
var flags = input.readUShortLE()
val compression = input.readUShortLE()
val modificationTime = input.readUShortLE()
val modificationDate = input.readUShortLE()
val crc32 = input.readUIntLE()
val compressedSize = input.readUIntLE()
val uncompressedSize = input.readUIntLE()
val fileNameLength = input.readUShortLE()
var fileName = ""
val extraFieldLength = input.readUShortLE()
var extraField = ByteArray(extraFieldLength.toInt())
val fileCommentLength = input.readUShortLE()
var fileComment = ""
val diskNumber = input.readUShortLE()
val internalAttributes = input.readUShortLE()
val externalAttributes = input.readUIntLE()
val localHeaderOffset = input.readUIntLE()
val variableFieldsLength =
fileNameLength.toInt() + extraFieldLength.toInt() + fileCommentLength.toInt()
if (variableFieldsLength > 0) {
val fileNameBytes = ByteArray(fileNameLength.toInt())
input.readFully(fileNameBytes)
fileName = fileNameBytes.toString(Charsets.UTF_8)
input.readFully(extraField)
val fileCommentBytes = ByteArray(fileCommentLength.toInt())
input.readFully(fileCommentBytes)
fileComment = fileCommentBytes.toString(Charsets.UTF_8)
}
flags = (flags and 0b1000u.inv()
.toUShort()) //disable data descriptor flag as they are not used
return ZipEntry(
version,
versionNeeded,
flags,
compression,
modificationTime,
modificationDate,
crc32,
compressedSize,
uncompressedSize,
diskNumber,
internalAttributes,
externalAttributes,
localHeaderOffset,
fileName,
extraField,
fileComment,
)
}
}
fun readLocalExtra(buffer: ByteBuffer) {
buffer.order(ByteOrder.LITTLE_ENDIAN)
localExtraField = ByteArray(buffer.getUShort().toInt())
}
fun toLFH(): ByteBuffer {
val nameBytes = fileName.toByteArray(Charsets.UTF_8)
val buffer = ByteBuffer.allocate(LFH_HEADER_SIZE + nameBytes.size + localExtraField.size)
.also { it.order(ByteOrder.LITTLE_ENDIAN) }
buffer.putUInt(LFH_SIGNATURE)
buffer.putUShort(versionNeeded)
buffer.putUShort(flags)
buffer.putUShort(compression)
buffer.putUShort(modificationTime)
buffer.putUShort(modificationDate)
buffer.putUInt(crc32)
buffer.putUInt(compressedSize)
buffer.putUInt(uncompressedSize)
buffer.putUShort(nameBytes.size.toUShort())
buffer.putUShort(localExtraField.size.toUShort())
buffer.put(nameBytes)
buffer.put(localExtraField)
buffer.flip()
return buffer
}
fun toCDE(): ByteBuffer {
val nameBytes = fileName.toByteArray(Charsets.UTF_8)
val commentBytes = fileComment.toByteArray(Charsets.UTF_8)
val buffer =
ByteBuffer.allocate(CDE_HEADER_SIZE + nameBytes.size + extraField.size + commentBytes.size)
.also { it.order(ByteOrder.LITTLE_ENDIAN) }
buffer.putUInt(CDE_SIGNATURE)
buffer.putUShort(version)
buffer.putUShort(versionNeeded)
buffer.putUShort(flags)
buffer.putUShort(compression)
buffer.putUShort(modificationTime)
buffer.putUShort(modificationDate)
buffer.putUInt(crc32)
buffer.putUInt(compressedSize)
buffer.putUInt(uncompressedSize)
buffer.putUShort(nameBytes.size.toUShort())
buffer.putUShort(extraField.size.toUShort())
buffer.putUShort(commentBytes.size.toUShort())
buffer.putUShort(diskNumber)
buffer.putUShort(internalAttributes)
buffer.putUInt(externalAttributes)
buffer.putUInt(localHeaderOffset)
buffer.put(nameBytes)
buffer.put(extraField)
buffer.put(commentBytes)
buffer.flip()
return buffer
}
}

View File

@ -0,0 +1,75 @@
package app.revanced.manager.patcher.signing
import com.android.apksig.ApkSigner
import org.bouncycastle.asn1.x500.X500Name
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
import org.bouncycastle.cert.X509v3CertificateBuilder
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.bouncycastle.operator.ContentSigner
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.math.BigInteger
import java.security.*
import java.security.cert.X509Certificate
import java.util.*
internal class Signer(
private val cn: String, password: String
) {
private val passwordCharArray = password.toCharArray()
private fun newKeystore(out: File) {
val (publicKey, privateKey) = createKey()
val privateKS = KeyStore.getInstance("BKS", "BC")
privateKS.load(null, passwordCharArray)
privateKS.setKeyEntry("alias", privateKey, passwordCharArray, arrayOf(publicKey))
privateKS.store(FileOutputStream(out), passwordCharArray)
}
private fun createKey(): Pair<X509Certificate, PrivateKey> {
val gen = KeyPairGenerator.getInstance("RSA")
gen.initialize(2048)
val pair = gen.generateKeyPair()
var serialNumber: BigInteger
do serialNumber =
BigInteger.valueOf(SecureRandom().nextLong()) while (serialNumber < BigInteger.ZERO)
val x500Name = X500Name("CN=$cn")
val builder = X509v3CertificateBuilder(
x500Name,
serialNumber,
Date(System.currentTimeMillis() - 1000L * 60L * 60L * 24L * 30L),
Date(System.currentTimeMillis() + 1000L * 60L * 60L * 24L * 366L * 30L),
Locale.ENGLISH,
x500Name,
SubjectPublicKeyInfo.getInstance(pair.public.encoded)
)
val signer: ContentSigner = JcaContentSignerBuilder("SHA256withRSA").build(pair.private)
return JcaX509CertificateConverter().getCertificate(builder.build(signer)) to pair.private
}
fun signApk(input: File, output: File) {
Security.addProvider(BouncyCastleProvider())
val ks = File(input.parent, "revanced-cli.keystore")
if (!ks.exists()) newKeystore(ks)
val keyStore = KeyStore.getInstance("BKS", "BC")
FileInputStream(ks).use { fis -> keyStore.load(fis, null) }
val alias = keyStore.aliases().nextElement()
val config = ApkSigner.SignerConfig.Builder(
cn,
keyStore.getKey(alias, passwordCharArray) as PrivateKey,
listOf(keyStore.getCertificate(alias) as X509Certificate)
).build()
val signer = ApkSigner.Builder(listOf(config))
signer.setCreatedBy(cn)
signer.setInputApk(input)
signer.setOutputApk(output)
signer.build().sign()
}
}

View File

@ -0,0 +1,175 @@
package app.revanced.manager.patcher.worker
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.graphics.drawable.Icon
import android.util.Log
import androidx.core.content.ContextCompat
import androidx.work.CoroutineWorker
import androidx.work.ForegroundInfo
import androidx.work.WorkerParameters
import app.revanced.manager.R
import app.revanced.manager.Variables.patches
import app.revanced.manager.Variables.selectedPatches
import app.revanced.manager.patcher.aapt.Aapt
import app.revanced.manager.patcher.aligning.ZipAligner
import app.revanced.manager.patcher.aligning.zip.ZipFile
import app.revanced.manager.patcher.aligning.zip.structures.ZipEntry
import app.revanced.manager.patcher.signing.Signer
import app.revanced.manager.ui.Resource
import app.revanced.patcher.Patcher
import app.revanced.patcher.PatcherOptions
import app.revanced.patcher.data.Data
import app.revanced.patcher.extensions.PatchExtensions.patchName
import app.revanced.patcher.logging.Logger
import app.revanced.patcher.patch.Patch
import java.io.File
class PatcherWorker(context: Context, parameters: WorkerParameters) :
CoroutineWorker(context, parameters) {
val tag = "ReVanced Manager"
private val workdir = File(inputData.getString("workdir")!!)
override suspend fun doWork(): Result {
if (runAttemptCount > 0) {
return Result.failure(
androidx.work.Data.Builder()
.putString("error", "Android requested retrying but retrying is disabled")
.build()
) // don't retry
}
val notificationIntent = Intent(applicationContext, PatcherWorker::class.java)
val pendingIntent: PendingIntent = PendingIntent.getActivity(
applicationContext, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE
)
val channel = NotificationChannel(
"revanced-patcher-patching", "Patching", NotificationManager.IMPORTANCE_LOW
)
val notificationManager =
ContextCompat.getSystemService(applicationContext, NotificationManager::class.java)
notificationManager!!.createNotificationChannel(channel)
val notification: Notification = Notification.Builder(applicationContext, channel.id)
.setContentTitle(applicationContext.getText(R.string.patcher_notification_title))
.setContentText(applicationContext.getText(R.string.patcher_notification_message))
.setLargeIcon(Icon.createWithResource(applicationContext, R.drawable.manager))
.setSmallIcon(Icon.createWithResource(applicationContext, R.drawable.manager))
.setContentIntent(pendingIntent).build()
setForeground(ForegroundInfo(1, notification))
return try {
runPatcher(workdir)
Result.success()
} catch (e: Exception) {
Log.e(tag, "Error while patching", e)
Result.failure(
androidx.work.Data.Builder()
.putString("error", "Error while patching: ${e.message ?: e::class.simpleName}")
.build()
)
}
}
private fun runPatcher(
workdir: File
): Boolean {
val aaptPath = Aapt.binary(applicationContext).absolutePath
val frameworkPath =
applicationContext.filesDir.resolve("framework").also { it.mkdirs() }.absolutePath
Log.d(tag, "Checking prerequisites")
val patches = findPatchesByIds(selectedPatches)
if (patches.isEmpty()) return true
Log.d(tag, "Creating directories")
File(inputData.getString("input")!!).copyTo(
applicationContext.filesDir.resolve("base.apk"),
true
)
val inputFile = File(applicationContext.filesDir, "base.apk")
val patchedFile = File(workdir, "patched.apk")
val outputFile = File(applicationContext.filesDir, "out.apk")
val cacheDirectory = workdir.resolve("cache")
val integrations = workdir.resolve("integrations.apk")
try {
Log.d(tag, "Creating patcher")
val patcher = Patcher(
PatcherOptions(
inputFile,
cacheDirectory.absolutePath,
patchResources = false,
aaptPath = aaptPath,
frameworkFolderLocation = frameworkPath,
logger = object : Logger {
override fun error(msg: String) {
Log.e(tag, msg)
}
override fun warn(msg: String) {
Log.w(tag, msg)
}
override fun info(msg: String) {
Log.i(tag, msg)
}
override fun trace(msg: String) {
Log.v(tag, msg)
}
}
)
)
Log.d(tag, "Adding ${patches.size} patch(es)")
patcher.addPatches(patches)
Log.d(tag, "Applying patches")
patcher.applyPatches().forEach { (patch, result) ->
if (result.isSuccess) {
Log.i(tag, "[success] $patch")
return@forEach
}
Log.e(tag, "[error] $patch:", result.exceptionOrNull()!!)
}
Log.d(tag, "Saving file")
val result = patcher.save() // this function uses quite a bit of resources
ZipFile(patchedFile).use { fs ->
result.dexFiles.forEach { it ->
Log.d(tag, "Writing dex file ${it.name}")
fs.addEntryCompressData(
ZipEntry.createWithName(it.name),
it.stream.readBytes()
)
result.resourceFile?.let {
fs.copyEntriesFromFileAligned(ZipFile(it), ZipAligner::getEntryAlignment)
}
fs.copyEntriesFromFileAligned(ZipFile(inputFile), ZipAligner::getEntryAlignment)
}
}
Log.d(tag, "Signing apk")
Signer("ReVanced", "s3cur3p@ssw0rd").signApk(patchedFile, outputFile)
Log.i(tag, "Successfully patched into $outputFile")
} finally {
Log.d(tag, "Deleting workdir")
workdir.deleteRecursively()
}
return false
}
private fun findPatchesByIds(ids: Iterable<String>): List<Class<out Patch<Data>>> {
val (patches) = patches.value as? Resource.Success ?: return listOf()
return patches.filter { patch -> ids.any { it == patch.patchName } }
}
}

View File

@ -5,6 +5,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.core.content.edit
import app.revanced.manager.ui.theme.Theme
import app.revanced.manager.util.ghIntegrations
import app.revanced.manager.util.ghPatches
import kotlin.reflect.KProperty
@ -13,7 +14,7 @@ class PreferencesManager(
sharedPreferences: SharedPreferences
) : BasePreferenceManager(sharedPreferences) {
var dynamicColor by booleanPreference("dynamic_color", true)
var theme by enumPreference("theme", Theme.SYSTEM)
var srcPatches by stringPreference("src_patches", ghPatches)
var srcIntegrations by stringPreference("src_integrations", ghIntegrations)
}

View File

@ -1,8 +1,8 @@
package app.revanced.manager.repository
import app.revanced.manager.dto.github.ApiCommit
import app.revanced.manager.dto.github.ApiContributor
import app.revanced.manager.dto.github.ApiRelease
import app.revanced.manager.dto.github.APICommit
import app.revanced.manager.dto.github.APIContributor
import app.revanced.manager.dto.github.APIRelease
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.request.*
@ -11,20 +11,19 @@ import kotlinx.coroutines.withContext
class GitHubRepository(private val client: HttpClient) {
suspend fun getLatestRelease(repo: String) = withContext(Dispatchers.IO) {
val res: List<ApiRelease> = client.get("$baseUrl/$repo/releases") {
val res: List<APIRelease> = client.get("$baseUrl/$repo/releases") {
parameter("per_page", 1)
}.body()
res.first()
}
suspend fun getLatestCommit(repo: String, ref: String) = withContext(Dispatchers.IO) {
client.get("$baseUrl/$repo/commits/$ref") {
parameter("per_page", 1)
}.body() as ApiCommit
}.body() as APICommit
}
suspend fun getContributors(org: String, repo: String) = withContext(Dispatchers.IO) {
client.get("$baseUrl/$org/$repo/contributors").body() as List<ApiContributor>
client.get("$baseUrl/$org/$repo/contributors").body() as List<APIContributor>
}
private companion object {

View File

@ -0,0 +1,10 @@
package app.revanced.manager.ui
sealed class Resource<out T> {
object Loading : Resource<Nothing>()
data class Success<out T>(val data: T) : Resource<T>()
companion object {
fun <T> success(value: T) = Success(value)
}
}

View File

@ -0,0 +1,18 @@
package app.revanced.manager.ui.component
import android.graphics.drawable.Drawable
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.google.accompanist.drawablepainter.rememberDrawablePainter
@Composable
fun AppIcon(drawable: Drawable?, contentDescription: String?) {
Image(
rememberDrawablePainter(drawable),
contentDescription,
Modifier.size(48.dp)
)
}

View File

@ -1,16 +1,12 @@
package app.revanced.manager.ui.component
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import app.revanced.manager.R
@Composable
fun ApplicationItem(

View File

@ -0,0 +1,50 @@
package app.revanced.manager.ui.component
import android.widget.Toast
import androidx.compose.material.ripple.LocalRippleTheme
import androidx.compose.material.ripple.RippleAlpha
import androidx.compose.material.ripple.RippleTheme
import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
@Composable
fun FloatingActionButton(
text: @Composable () -> Unit,
icon: @Composable () -> Unit,
onClick: () -> Unit,
enabled: Boolean
) {
val context = LocalContext.current
// TODO: set icon color:
// tint = if (enabled) LocalContentColor.current.copy(alpha = LocalContentAlpha.current) else else DarkGray
CompositionLocalProvider(
LocalRippleTheme provides if (enabled) {
LocalRippleTheme.current
} else NoRippleTheme
) {
ExtendedFloatingActionButton(
text = text,
icon = icon,
onClick = {
if (!enabled) {
Toast.makeText(context, "Please select an application.", Toast.LENGTH_SHORT)
}
if (enabled) onClick()
},
containerColor = if (enabled) MaterialTheme.colorScheme.primaryContainer else Color.Gray,
)
}
}
private object NoRippleTheme : RippleTheme {
@Composable
override fun defaultColor() = Color.Unspecified
@Composable
override fun rippleAlpha(): RippleAlpha = RippleAlpha(0.0f, 0.0f, 0.0f, 0.0f)
}

View File

@ -0,0 +1,34 @@
package app.revanced.manager.ui.component
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
@Composable
fun GroupHeader(
title: String,
color: Color = MaterialTheme.colorScheme.primary
) {
Box(
Modifier
.padding(start = 12.dp)
.fillMaxWidth(),
contentAlignment = Alignment.CenterStart
) {
Text(
text = title,
color = color,
fontSize = LocalTextStyle.current.fontSize.times(0.95f),
fontWeight = FontWeight.SemiBold
)
}
}

View File

@ -0,0 +1,27 @@
package app.revanced.manager.ui.component
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import app.revanced.manager.R
@Composable
fun LoadingIndicator(id: Int? = null) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(stringResource(R.string.loading_body))
if (id != null) Text(stringResource(id))
CircularProgressIndicator(modifier = Modifier.padding(vertical = 16.dp))
}
}

View File

@ -0,0 +1,40 @@
package app.revanced.manager.ui.component
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import app.revanced.manager.R
import app.revanced.manager.ui.viewmodel.PatchClass
import app.revanced.manager.ui.viewmodel.PatcherViewModel
import app.revanced.patcher.annotation.Package
import app.revanced.patcher.extensions.PatchExtensions.compatiblePackages
import org.koin.androidx.compose.getViewModel
@Composable
fun PatchCompatibilityDialog(
patchClass: PatchClass, pvm: PatcherViewModel = getViewModel(), onClose: () -> Unit
) {
val patch = patchClass.patch
val packageName = pvm.getSelectedPackageInfo()?.packageName
AlertDialog(onDismissRequest = onClose, shape = RoundedCornerShape(12.dp), title = {
Text(stringResource(id = R.string.unsupported), textAlign = TextAlign.Center)
}, text = {
(patch.compatiblePackages!!.forEach { p: Package ->
if (p.name == packageName) {
Text(
stringResource(id = R.string.only_compatible) + p.versions.reversed()
.joinToString(", ")
)
}
})
}, confirmButton = {
TextButton(onClick = onClose) {
Text(text = "Dismiss")
}
})
}

View File

@ -13,7 +13,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import app.revanced.manager.R
import app.revanced.manager.util.ghOrganization
import app.revanced.manager.util.openUrl
@ -34,7 +33,6 @@ fun SocialItem(@StringRes label: Int, vec: ImageVector, fn: () -> Unit) {
)
}
@Preview
@Composable
fun SocialItemPreview() {
val ctx = LocalContext.current.applicationContext

View File

@ -15,6 +15,12 @@ import kotlinx.parcelize.RawValue
sealed interface AppDestination : Destination {
@Parcelize
object Dashboard : AppDestination
@Parcelize
object AppSelector : AppDestination
@Parcelize
object PatchSelector : AppDestination
}
@Parcelize

View File

@ -91,46 +91,24 @@ fun DashboardScreen(viewModel: DashboardViewModel = getViewModel()) {
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
ApplicationItem(
name = "ReVanced",
released = "com.google.android.youtube",
name = "Youtube ReVanced",
released = "Released [who knows] centuries ago",
icon = { Icon(Icons.Default.Dashboard, "ReVanced") }
) {
ChangelogText(
"""
fix: aaaaaa
fix: aaaaaa
fix: aaaaaa
fix: aaaaaa
fix: aaaaaa
cossal will explode
""".trimIndent()
)
}
ApplicationItem(
name = "ReReddit",
released = "Released 1 month ago",
name = "Reddit ReVanced",
released = "Released [REDACTED] month ago",
icon = { Icon(Icons.Default.Build, "ReReddit") }
) {
ChangelogText(
"""
fix: bbbbbb
fix: bbbbbb
fix: bbbbbb
fix: bbbbbb
fix: bbbbbb
fix: bbbbbb
fix: bbbbbb
fix: bbbbbb
fix: bbbbbb
fix: bbbbbb
fix: bbbbbb
fix: bbbbbb
fix: bbbbbb
fix: bbbbbb
fix: bbbbbb
fix: bbbbbb
fix: bbbbbb
fix: bbbbbb
fix: bbbbbb
hi ushie
""".trimIndent()
)
}

View File

@ -1,6 +1,9 @@
package app.revanced.manager.ui.screen
import androidx.compose.animation.*
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.with
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
@ -19,8 +22,8 @@ import com.xinto.taxi.rememberNavigator
@Composable
fun MainDashboardScreen(navigator: BackstackNavigator<AppDestination>) {
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(
decayAnimationSpec = rememberSplineBasedDecay(),
state = rememberTopAppBarState()
state = rememberTopAppBarState(),
canScroll = { true }
)
val mainRootNavigator = rememberNavigator(DashboardDestination.DASHBOARD)
@ -31,7 +34,7 @@ fun MainDashboardScreen(navigator: BackstackNavigator<AppDestination>) {
.fillMaxSize()
.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
MediumTopAppBar(
LargeTopAppBar(
title = {
Text(
text = stringResource(mainRootNavigator.currentDestination.label),
@ -64,7 +67,10 @@ fun MainDashboardScreen(navigator: BackstackNavigator<AppDestination>) {
) { destination ->
when (destination) {
DashboardDestination.DASHBOARD -> DashboardScreen()
DashboardDestination.PATCHER -> PatcherScreen()
DashboardDestination.PATCHER -> PatcherScreen(
onClickAppSelector = { navigator.push(AppDestination.AppSelector) },
onClickPatchSelector = { navigator.push(AppDestination.PatchSelector) }
)
DashboardDestination.SETTINGS -> SettingsScreen()
}
}

View File

@ -0,0 +1,88 @@
package app.revanced.manager.ui.screen
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.LinearOutSlowInEasing
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.FolderZip
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import app.revanced.manager.ui.viewmodel.PatcherViewModel
import org.koin.androidx.compose.getViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun NewPatcherScreen(
onClickAppSelector: () -> Unit,
onClickPatchSelector: () -> Unit,
viewModel: PatcherViewModel = getViewModel()
) {
var validBundle = false
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
) {
ElevatedCard(
onClick = onClickAppSelector,
modifier = Modifier
.padding(16.dp, 4.dp)
.animateContentSize(
animationSpec = tween(
durationMillis = 300,
easing = LinearOutSlowInEasing
)
),
) {
Column(
modifier = Modifier.padding(12.dp, 8.dp, 12.dp, 8.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.Start
) {
Row(modifier = Modifier.padding(0.dp, 12.dp)) {
Icon(
Icons.Default.FolderZip,
"Patch Bundle",
modifier = Modifier
.size(40.dp)
.align(Alignment.CenterVertically)
)
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.Start,
modifier = Modifier.padding(4.dp)
) {
if (!validBundle) {
Text(
text = "Select a patch bundle",
fontWeight = FontWeight.Bold,
fontSize = 13.sp
)
Text(text = "(not selected)", fontSize = 13.sp)
} else {
Text(
text = "Selected patch bundle",
fontWeight = FontWeight.Bold,
fontSize = 13.sp
)
Text(
text = viewModel.getSelectedPackageInfo()!!.applicationInfo.name,
fontSize = 13.sp
)
}
}
Spacer(Modifier.weight(1f, true))
}
}
}
}
}

View File

@ -1,155 +1,96 @@
package app.revanced.manager.ui.screen
import android.widget.Toast
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Build
import androidx.compose.material.icons.filled.Dashboard
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import app.revanced.manager.R
import app.revanced.manager.ui.component.ApplicationItem
import app.revanced.manager.ui.component.HeadlineWithCard
import app.revanced.manager.ui.viewmodel.DashboardViewModel
import app.revanced.manager.Variables.patches
import app.revanced.manager.Variables.selectedAppPackage
import app.revanced.manager.Variables.selectedPatches
import app.revanced.manager.ui.Resource
import app.revanced.manager.ui.component.FloatingActionButton
import app.revanced.manager.ui.viewmodel.PatcherViewModel
import org.koin.androidx.compose.getViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PatcherScreen(viewModel: DashboardViewModel = getViewModel()) {
val context = LocalContext.current
val padHoriz = 16.dp
val padVert = 10.dp
fun PatcherScreen(
onClickAppSelector: () -> Unit,
onClickPatchSelector: () -> Unit,
viewModel: PatcherViewModel = getViewModel()
) {
val selectedAmount = selectedPatches.size
val selectedAppPackage by selectedAppPackage
val hasAppSelected = selectedAppPackage.isPresent
val patchesLoaded = patches.value is Resource.Success
Column(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 18.dp)
.verticalScroll(state = rememberScrollState()),
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
HeadlineWithCard(R.string.updates) {
Row(
Scaffold(floatingActionButton = {
FloatingActionButton(
enabled = hasAppSelected && viewModel.anyPatchSelected(),
onClick = { viewModel.startPatcher() },
icon = { Icon(Icons.Default.Build, contentDescription = "Patch") },
text = { Text(text = "Patch") }
)
}) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(16.dp),
) {
Card(
modifier = Modifier
.padding(horizontal = padHoriz, vertical = padVert)
.padding(4.dp)
.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
enabled = patchesLoaded,
onClick = onClickAppSelector
) {
Column {
CommitDate(
label = R.string.patcher,
date = viewModel.patcherCommitDate
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = stringResource(id = R.string.card_application_header),
style = MaterialTheme.typography.titleMedium
)
CommitDate(
label = R.string.manager,
date = viewModel.managerCommitDate
Text(
text = if (patchesLoaded) {
selectedAppPackage.orElse(stringResource(R.string.card_application_not_selected))
} else {
stringResource(R.string.card_application_not_loaded)},
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(0.dp, 8.dp)
)
}
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Button(
enabled = false, // needs update
onClick = {
Toast.makeText(context, "Already up-to-date!", Toast.LENGTH_SHORT)
.show()
}
Card(
modifier = Modifier
.padding(4.dp)
.fillMaxWidth(),
enabled = hasAppSelected,
onClick = onClickPatchSelector
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = stringResource(R.string.card_patches_header),
style = MaterialTheme.typography.titleMedium
)
Text(
text = if (!hasAppSelected) {
"Select an application first."
} else if (viewModel.anyPatchSelected()) {
"$selectedAmount patches selected."
} else {
stringResource(R.string.card_patches_body_patches)
},
) { Text(stringResource(R.string.update_patch_bundle)) }
Text(
text = "No updates available",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
fontSize = 10.sp,
)
}
}
}
HeadlineWithCard(R.string.patched_apps) {
Row(
modifier = Modifier
.padding(horizontal = padHoriz, vertical = padVert)
.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Column {
val amount = 2 // TODO
Text(
text = "${stringResource(R.string.updates_available)}: $amount",
style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.Bold,
fontSize = 18.sp
)
}
Button(
enabled = true, // needs update
onClick = {
Toast.makeText(context, "Already up-to-date!", Toast.LENGTH_SHORT).show()
}
) { Text(stringResource(R.string.update_all)) }
}
Column(
modifier = Modifier
.padding(horizontal = padHoriz)
.padding(bottom = padVert)
.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
ApplicationItem(
name = "ReVanced",
released = "Released 2 days ago",
icon = { Icon(Icons.Default.Dashboard, "ReVanced") }
) {
ChangelogText(
"""
fix: aaaaaa
fix: aaaaaa
fix: aaaaaa
fix: aaaaaa
fix: aaaaaa
""".trimIndent()
)
}
ApplicationItem(
name = "ReReddit",
released = "Released 1 month ago",
icon = { Icon(Icons.Default.Build, "ReReddit") }
) {
ChangelogText(
"""
fix: bbbbbb
fix: bbbbbb
fix: bbbbbb
fix: bbbbbb
fix: bbbbbb
fix: bbbbbb
fix: bbbbbb
fix: bbbbbb
fix: bbbbbb
fix: bbbbbb
fix: bbbbbb
fix: bbbbbb
fix: bbbbbb
fix: bbbbbb
fix: bbbbbb
fix: bbbbbb
fix: bbbbbb
fix: bbbbbb
fix: bbbbbb
""".trimIndent()
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(0.dp, 8.dp)
)
}
}

View File

@ -1,26 +1,27 @@
package app.revanced.manager.ui.screen
import android.os.Build
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Code
import androidx.compose.material.icons.filled.NavigateBefore
import androidx.compose.material.icons.filled.Palette
import androidx.compose.material.icons.filled.Style
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import app.revanced.manager.R
import app.revanced.manager.preferences.PreferencesManager
import app.revanced.manager.ui.component.GroupHeader
import app.revanced.manager.ui.component.SocialItem
import app.revanced.manager.ui.navigation.AppDestination
import app.revanced.manager.ui.theme.Theme
import app.revanced.manager.ui.viewmodel.SettingsViewModel
import com.xinto.taxi.BackstackNavigator
import org.koin.androidx.compose.get
import org.koin.androidx.compose.getViewModel
@OptIn(ExperimentalMaterial3Api::class)
@ -35,18 +36,82 @@ fun SettingsScreen(viewModel: SettingsViewModel = getViewModel()) {
.verticalScroll(state = rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
if (viewModel.showThemePicker) {
ThemePicker(
onDismissRequest = viewModel::dismissThemePicker,
onConfirm = viewModel::setTheme
)
}
GroupHeader(title = "Appearance")
ListItem(
modifier = Modifier.clickable { prefs.dynamicColor = !prefs.dynamicColor },
headlineText = { Text(stringResource(R.string.dynamic_color)) },
trailingContent = {
Switch(
checked = prefs.dynamicColor,
onCheckedChange = { prefs.dynamicColor = it }
)
}
modifier = Modifier.clickable { viewModel.showThemePicker() },
headlineText = { Text(stringResource(R.string.theme)) },
leadingContent = { Icon(Icons.Default.Style, contentDescription = null) },
trailingContent = { FilledTonalButton(onClick = { viewModel.showThemePicker() }) {
Text(text = prefs.theme.displayName)
} }
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
ListItem(
modifier = Modifier.clickable { prefs.dynamicColor = !prefs.dynamicColor },
headlineText = { Text(stringResource(R.string.dynamic_color)) },
leadingContent = { Icon(Icons.Default.Palette, contentDescription = null) },
trailingContent = {
Switch(
checked = prefs.dynamicColor,
onCheckedChange = { prefs.dynamicColor = it }
)
}
)
}
Divider()
SocialItem(R.string.github, Icons.Default.Code, viewModel::openGitHub)
}
}
@Composable
fun ThemePicker(
onDismissRequest: () -> Unit,
onConfirm: (Theme) -> Unit,
prefs: PreferencesManager = get()
) {
var selectedTheme by remember { mutableStateOf(prefs.theme) }
AlertDialog(
onDismissRequest = onDismissRequest,
title = { Text(stringResource(R.string.theme)) },
text = {
Column {
Theme.values().forEach { theme ->
Row(
modifier = Modifier.clickable { selectedTheme = theme },
verticalAlignment = Alignment.CenterVertically
) {
Text(
theme.displayName,
style = MaterialTheme.typography.labelLarge
)
Spacer(Modifier.weight(1f, true))
RadioButton(
selected = theme == selectedTheme,
onClick = { selectedTheme = theme }
)
}
}
}
},
confirmButton = {
Button(
onClick = {
onConfirm(selectedTheme)
onDismissRequest()
}
) {
Text(stringResource(R.string.apply))
}
}
)
}

View File

@ -0,0 +1,77 @@
package app.revanced.manager.ui.screen.subscreens
import android.annotation.SuppressLint
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import app.revanced.manager.R
import app.revanced.manager.Variables.filteredApps
import app.revanced.manager.Variables.patchesState
import app.revanced.manager.ui.Resource
import app.revanced.manager.ui.component.AppIcon
import app.revanced.manager.ui.component.LoadingIndicator
import app.revanced.manager.ui.navigation.AppDestination
import app.revanced.manager.ui.viewmodel.AppSelectorViewModel
import com.xinto.taxi.BackstackNavigator
import org.koin.androidx.compose.getViewModel
@OptIn(ExperimentalMaterial3Api::class)
@SuppressLint("QueryPermissionsNeeded")
@Composable
fun AppSelectorSubscreen(
navigator: BackstackNavigator<AppDestination>,
vm: AppSelectorViewModel = getViewModel(),
) {
Scaffold(
topBar = {
MediumTopAppBar(
title = { Text(stringResource(R.string.app_selector_title)) },
navigationIcon = {
IconButton(onClick = navigator::pop) {
Icon(
imageVector = Icons.Default.ArrowBack,
contentDescription = null
)
}
},
)
}
) { paddingValues ->
when (patchesState) {
is Resource.Success -> {
LazyColumn(modifier = Modifier.padding(paddingValues)) {
items(count = filteredApps.size) {
val app = filteredApps[it]
val label = vm.applicationLabel(app)
val packageName = app.packageName
val same = packageName == label
ListItem(modifier = Modifier.clickable {
vm.setSelectedAppPackage(app.packageName)
navigator.pop()
}, leadingContent = {
AppIcon(vm.loadIcon(app), packageName)
}, headlineText = {
if (same) {
Text(packageName)
} else {
Text(label)
}
}, supportingText = {
if (!same) {
Text(packageName)
}
})
}
}
}
else -> LoadingIndicator(null)
}
}
}

View File

@ -0,0 +1,244 @@
package app.revanced.manager.ui.screen.subscreens
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import app.revanced.manager.R
import app.revanced.manager.Variables.patchesState
import app.revanced.manager.ui.Resource
import app.revanced.manager.ui.component.LoadingIndicator
import app.revanced.manager.ui.component.PatchCompatibilityDialog
import app.revanced.manager.ui.navigation.AppDestination
import app.revanced.manager.ui.theme.Typography
import app.revanced.manager.ui.viewmodel.PatchClass
import app.revanced.manager.ui.viewmodel.PatcherViewModel
import app.revanced.patcher.extensions.PatchExtensions.description
import app.revanced.patcher.extensions.PatchExtensions.patchName
import app.revanced.patcher.extensions.PatchExtensions.version
import com.xinto.taxi.BackstackNavigator
import org.koin.androidx.compose.getViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PatchesSelectorSubscreen(
navigator: BackstackNavigator<AppDestination>,
pvm: PatcherViewModel = getViewModel(),
) {
val patches = rememberSaveable { pvm.getFilteredPatchesAndCheckOptions() }
var query by mutableStateOf("")
Scaffold(
topBar = {
MediumTopAppBar(
title = {
Text(
text = stringResource(R.string.card_patches_header),
style = MaterialTheme.typography.headlineLarge
)
},
navigationIcon = {
IconButton(onClick = navigator::pop) {
Icon(
imageVector = Icons.Default.ArrowBack,
contentDescription = null
)
}
},
actions = {
IconButton(onClick = {
pvm.selectAllPatches(patches, !pvm.anyPatchSelected())
}) {
if (!pvm.anyPatchSelected()) Icon(
Icons.Default.SelectAll,
contentDescription = null
) else Icon(Icons.Default.Deselect, contentDescription = null)
}
}
)
},
) { paddingValues ->
Column(
modifier = Modifier
.padding(paddingValues)
) {
when (patchesState) {
is Resource.Success -> {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp, 4.dp),
) {
Row(
modifier = Modifier.fillMaxWidth()
) {
OutlinedTextField(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
shape = RoundedCornerShape(12.dp),
value = query,
onValueChange = { newValue ->
query = newValue
},
leadingIcon = {
Icon(Icons.Default.Search, "Search")
},
trailingIcon = {
if (query.isNotEmpty()) {
IconButton(onClick = {
query = ""
}) {
Icon(Icons.Default.Clear, "Clear")
}
}
},
)
}
}
LazyColumn(Modifier.padding(0.dp, 2.dp)) {
if (query.isEmpty() || query.isBlank()) {
items(count = patches.size) {
val patch = patches[it]
val name = patch.patch.patchName
PatchCard(patch, pvm.isPatchSelected(name)) {
pvm.selectPatch(name, !pvm.isPatchSelected(name))
}
}
} else {
items(count = patches.size) {
val patch = patches[it]
val name = patch.patch.patchName
if (name.contains(query.lowercase())) {
PatchCard(patch, pvm.isPatchSelected(name)) {
pvm.selectPatch(name, !pvm.isPatchSelected(name))
}
}
}
}
}
}
else -> LoadingIndicator(null)
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PatchCard(patchClass: PatchClass, isSelected: Boolean, onSelected: () -> Unit) {
val patch = patchClass.patch
val name = patch.patchName
var showDialog by remember { mutableStateOf(false) }
ElevatedCard(
modifier = Modifier
.padding(16.dp, 4.dp),
enabled = !patchClass.unsupported,
onClick = onSelected
) {
Column(modifier = Modifier.padding(12.dp, 0.dp, 12.dp, 12.dp)) {
Row {
Column(
Modifier
.align(Alignment.CenterVertically)
) {
Text(
text = name.replace("-", " ").split(" ")
.joinToString(" ") { it.replaceFirstChar(Char::uppercase) },
style = MaterialTheme.typography.titleMedium
)
}
Spacer(Modifier.width(4.dp))
Row(
Modifier
.align(Alignment.CenterVertically)
) {
Text(
text = patch.version ?: "unknown",
style = Typography.bodySmall
)
}
Spacer(Modifier.weight(1f, true))
Column(modifier = Modifier.padding(0.dp, 6.dp)) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(4.dp)
) {
if (patchClass.hasPatchOptions) {
CompositionLocalProvider(LocalMinimumTouchTargetEnforcement provides false) {
IconButton(onClick = { }, modifier = Modifier.size(24.dp)) {
Icon(
Icons.Outlined.Settings,
contentDescription = "Patch Options"
)
}
}
}
Spacer(Modifier.width(8.dp))
CompositionLocalProvider(LocalMinimumTouchTargetEnforcement provides false) {
Checkbox(
enabled = !patchClass.unsupported,
checked = isSelected,
onCheckedChange = { onSelected() }
)
}
}
}
}
var isExpanded by remember { mutableStateOf(false) }
patch.description?.let { desc ->
Text(
text = desc,
modifier = Modifier
.padding(0.dp, 8.dp, 22.dp, 8.dp)
.clickable { isExpanded = !isExpanded },
maxLines = if (isExpanded) Int.MAX_VALUE else 1,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.bodyMedium
)
}
if (patchClass.unsupported) {
CompositionLocalProvider(LocalMinimumTouchTargetEnforcement provides false) {
Column {
Row {
if (showDialog) {
PatchCompatibilityDialog(
onClose = { showDialog = false },
patchClass = patchClass,
)
}
InputChip(
selected = false,
onClick = { showDialog = true },
leadingIcon = {
Icon(
Icons.Default.Warning,
tint = MaterialTheme.colorScheme.primary,
contentDescription = stringResource(id = R.string.unsupported_version)
)
},
label = { Text(stringResource(id = R.string.unsupported_version)) }
)
}
}
}
}
}
}
}

View File

@ -49,3 +49,8 @@ fun ReVancedManagerTheme(
content = content
)
}
enum class Theme(val displayName: String) {
SYSTEM("System"),
LIGHT("Light"),
DARK("Dark");
}

View File

@ -0,0 +1,61 @@
package app.revanced.manager.ui.viewmodel
import android.app.Application
import android.content.pm.ApplicationInfo
import android.graphics.drawable.Drawable
import android.util.Log
import androidx.lifecycle.ViewModel
import app.revanced.manager.Variables
import app.revanced.manager.Variables.filteredApps
import app.revanced.manager.Variables.patches
import app.revanced.manager.Variables.selectedAppPackage
import app.revanced.manager.ui.Resource
import app.revanced.patcher.extensions.PatchExtensions.compatiblePackages
import java.util.*
class AppSelectorViewModel(
val app: Application,
) : ViewModel() {
init {
filterApps()
}
private fun filterApps(): List<ApplicationInfo> {
try {
val (patches) = patches.value as Resource.Success
patches.forEach patch@{ patch ->
patch.compatiblePackages?.forEach { pkg ->
try {
val appInfo = app.packageManager.getApplicationInfo(pkg.name, 0)
if (appInfo !in filteredApps) {
filteredApps.add(appInfo)
return@forEach
}
} catch (e: Exception) {
return@forEach
}
}
}
} catch (e: Exception) {
Log.e("ReVanced Manager", "An error occurred while filtering", e)
}
return emptyList()
}
fun applicationLabel(info: ApplicationInfo): String {
return app.packageManager.getApplicationLabel(info).toString()
}
fun loadIcon(info: ApplicationInfo): Drawable? {
return info.loadIcon(app.packageManager)
}
fun setSelectedAppPackage(appId: String) {
selectedAppPackage.value.ifPresent { s ->
if (s != appId) Variables.selectedPatches.clear()
}
selectedAppPackage.value = Optional.of(appId)
}
}

View File

@ -0,0 +1,14 @@
package app.revanced.manager.ui.viewmodel
import android.app.Application
import android.content.pm.ApplicationInfo
import android.graphics.drawable.Drawable
import androidx.lifecycle.ViewModel
class NewPatcherScreenViewModel(
val app: Application,
) : ViewModel() {
fun loadIcon(info: ApplicationInfo): Drawable? {
return info.loadIcon(app.packageManager)
}
}

View File

@ -1,40 +1,164 @@
package app.revanced.manager.ui.viewmodel
import android.text.format.DateUtils
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import android.app.Application
import android.content.pm.PackageManager
import android.os.Parcelable
import android.util.Log
import androidx.lifecycle.ViewModel
import app.revanced.manager.repository.GitHubRepository
import app.revanced.manager.util.ghManager
import app.revanced.manager.util.ghPatcher
import androidx.lifecycle.viewModelScope
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequest
import androidx.work.OutOfQuotaPolicy
import androidx.work.WorkManager
import app.revanced.manager.Variables.patches
import app.revanced.manager.Variables.selectedAppPackage
import app.revanced.manager.Variables.selectedPatches
import app.revanced.manager.api.API
import app.revanced.manager.patcher.worker.PatcherWorker
import app.revanced.manager.ui.Resource
import app.revanced.patcher.data.Data
import app.revanced.patcher.extensions.PatchExtensions.compatiblePackages
import app.revanced.patcher.extensions.PatchExtensions.options
import app.revanced.patcher.extensions.PatchExtensions.patchName
import app.revanced.patcher.patch.Patch
import app.revanced.patcher.util.patch.impl.DexPatchBundle
import dalvik.system.DexClassLoader
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import java.text.SimpleDateFormat
import java.util.*
import kotlinx.parcelize.Parcelize
import java.io.File
class PatcherViewModel(private val repository: GitHubRepository) : ViewModel() {
var patcherCommitDate by mutableStateOf("")
private set
var managerCommitDate by mutableStateOf("")
private set
class PatcherViewModel(private val app: Application, private val api: API) : ViewModel() {
private val workdir = createWorkDir()
private lateinit var patchBundleFile: String
private val tag = "ReVanced Manager"
init {
runBlocking {
patcherCommitDate = commitDateOf(ghPatcher)
managerCommitDate = commitDateOf(ghManager)
loadPatches()
downloadIntegrations()
}
}
private suspend fun commitDateOf(repo: String, ref: String = "HEAD"): String {
val commit = repository.getLatestCommit(repo, ref).commit
return DateUtils.getRelativeTimeSpanString(
formatter.parse(commit.committer.date)!!.time,
Calendar.getInstance().timeInMillis,
DateUtils.MINUTE_IN_MILLIS
).toString()
fun selectPatch(patchId: String, state: Boolean) {
if (state) selectedPatches.add(patchId)
else selectedPatches.remove(patchId)
}
private companion object {
val formatter = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.getDefault())
private suspend fun downloadIntegrations() {
api.downloadIntegrations(workdir).renameTo(File(workdir,"integrations.apk"))
}
fun selectAllPatches(patchList: List<PatchClass>, selectAll: Boolean) {
patchList.forEach { patch ->
val patchId = patch.patch.patchName
if (selectAll && !patch.unsupported) selectedPatches.add(patchId)
else selectedPatches.remove(patchId)
}
}
fun setOption(patch: PatchClass, key: String, value: String) {
patch.patch.options?.set(key, value)
for (option in patch.patch.options!!) {
println(option.key + option.value + option.title + option.description)
}
}
fun getOption(patch: PatchClass, key: String) {
patch.patch.options?.get(key)
}
fun isPatchSelected(patchId: String): Boolean {
return selectedPatches.contains(patchId)
}
fun anyPatchSelected(): Boolean {
return !selectedPatches.isEmpty()
}
fun getSelectedPackageInfo() =
if (selectedAppPackage.value.isPresent)
app.packageManager.getPackageInfo(
selectedAppPackage.value.get(),
PackageManager.GET_META_DATA
)
else null
fun getFilteredPatchesAndCheckOptions(): List<PatchClass> {
return buildList {
val selected = getSelectedPackageInfo() ?: return@buildList
val (patches) = patches.value as? Resource.Success ?: return@buildList
patches.forEach patch@{ patch ->
var unsupported = false
var hasPatchOptions = false
if (patch.options != null) {
hasPatchOptions = true
Log.d(tag, "${patch.patchName} has patch options.")
}
patch.compatiblePackages?.forEach { pkg ->
// if we detect unsupported once, don't overwrite it
if (pkg.name == selected.packageName) {
if (!unsupported)
unsupported =
pkg.versions.isNotEmpty() && !pkg.versions.any { it == selected.versionName }
add(PatchClass(patch, unsupported, hasPatchOptions))
}
}
}
}
}
private fun loadPatches() = viewModelScope.launch {
try {
val file = api.downloadPatchBundle(app.filesDir)
patchBundleFile = file.absolutePath
loadPatches0(file.absolutePath)
} catch (e: Exception) {
Log.e("ReVancedManager", "An error occurred while loading patches", e)
}
}
private fun loadPatches0(path: String) {
val patchClasses = DexPatchBundle(
path, DexClassLoader(
path,
app.codeCacheDir.absolutePath,
null,
javaClass.classLoader
)
).loadPatches()
patches.value = Resource.Success(patchClasses)
Log.d("ReVanced Manager", "Finished loading patches")
}
fun startPatcher() {
WorkManager
.getInstance(app)
.enqueueUniqueWork(
"patching",
ExistingWorkPolicy.KEEP,
OneTimeWorkRequest.Builder(PatcherWorker::class.java)
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
.setInputData(
androidx.work.Data.Builder()
.putString("workdir", workdir.toString())
.put("input",
getSelectedPackageInfo()?.applicationInfo?.publicSourceDir
)
.build()).build()
)
}
private fun createWorkDir(): File {
return app.filesDir.resolve("tmp-${System.currentTimeMillis()}")
.also { it.mkdirs() }
}
}
@Parcelize
data class PatchClass(
val patch: Class<out Patch<Data>>,
val unsupported: Boolean,
val hasPatchOptions: Boolean,
) : Parcelable

View File

@ -1,8 +1,12 @@
package app.revanced.manager.ui.viewmodel
import android.app.Application
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import app.revanced.manager.preferences.PreferencesManager
import app.revanced.manager.ui.theme.Theme
import app.revanced.manager.util.ghOrganization
import app.revanced.manager.util.openUrl
@ -10,5 +14,19 @@ class SettingsViewModel(
private val app: Application,
val prefs: PreferencesManager
) : ViewModel() {
var showThemePicker by mutableStateOf(false)
private set
fun showThemePicker() {
showThemePicker = true
}
fun dismissThemePicker() {
showThemePicker = false
}
fun setTheme(theme: Theme) {
prefs.theme = theme
}
fun openGitHub() = app.openUrl(ghOrganization)
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -2,29 +2,48 @@
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
android:viewportWidth="278"
android:viewportHeight="278">
<group android:scaleX="0.75"
android:scaleY="0.75"
android:translateX="34.75"
android:translateY="34.75">
<path
android:pathData="M0.06,0.83h277v277h-277z"
android:fillColor="#111623"/>
<path
android:pathData="M108.01,80.13H174.31V95.81H108.01V80.13ZM96.91,95.81V80.13C96.91,73.98 101.88,69 108.01,69H174.31C180.44,69 185.41,73.98 185.41,80.13V95.81H189.78C198.97,95.81 206.43,103.29 206.43,112.5V189.31C206.43,198.53 198.97,206 189.78,206H92.32C83.12,206 75.67,198.53 75.67,189.31V184.16H127.18C133.31,184.16 138.28,179.18 138.28,173.03V164.13C138.28,157.98 133.31,153 127.18,153H75.67V112.5C75.67,103.29 83.12,95.81 92.32,95.81H96.91Z"
android:fillColor="#ffffff"
android:fillType="evenOdd"/>
<path
android:pathData="M75.67,184.16H56.53C50.4,184.16 45.43,179.18 45.43,173.03V164.13C45.43,157.98 50.4,153 56.53,153H75.67V184.16Z"
android:fillType="evenOdd">
<aapt:attr name="android:fillColor">
<gradient
android:startX="45.43"
android:startY="132.25"
android:endX="87.19"
android:endY="221.04"
android:type="linear">
<item android:offset="0" android:color="#FF353DFF"/>
<item android:offset="1" android:color="#FF3DDCFF"/>
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
android:pathData="M75.67,184.16H56.53C50.4,184.16 45.43,179.18 45.43,173.03V164.13C45.43,157.98 50.4,153 56.53,153H75.67V184.16Z"
android:fillType="evenOdd">
<aapt:attr name="android:fillColor">
<gradient
android:startX="45.43"
android:startY="132.25"
android:endX="87.19"
android:endY="221.04"
android:type="linear">
<item android:offset="0" android:color="#FF353DFF"/>
<item android:offset="1" android:color="#FF3DDCFF"/>
</gradient>
</aapt:attr>
</path>
</group>
</vector>

View File

@ -0,0 +1,44 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="278dp"
android:height="278dp"
android:viewportWidth="278"
android:viewportHeight="278">
<path
android:pathData="M0.06,0.83h277v277h-277z"
android:fillColor="#111623"/>
<path
android:pathData="M108.01,80.13H174.31V95.81H108.01V80.13ZM96.91,95.81V80.13C96.91,73.98 101.88,69 108.01,69H174.31C180.44,69 185.41,73.98 185.41,80.13V95.81H189.78C198.97,95.81 206.43,103.29 206.43,112.5V189.31C206.43,198.53 198.97,206 189.78,206H92.32C83.12,206 75.67,198.53 75.67,189.31V184.16H127.18C133.31,184.16 138.28,179.18 138.28,173.03V164.13C138.28,157.98 133.31,153 127.18,153H75.67V112.5C75.67,103.29 83.12,95.81 92.32,95.81H96.91Z"
android:fillColor="#ffffff"
android:fillType="evenOdd"/>
<path
android:pathData="M75.67,184.16H56.53C50.4,184.16 45.43,179.18 45.43,173.03V164.13C45.43,157.98 50.4,153 56.53,153H75.67V184.16Z"
android:fillType="evenOdd">
<aapt:attr name="android:fillColor">
<gradient
android:startX="45.43"
android:startY="132.25"
android:endX="87.19"
android:endY="221.04"
android:type="linear">
<item android:offset="0" android:color="#FF353DFF"/>
<item android:offset="1" android:color="#FF3DDCFF"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M75.67,184.16H56.53C50.4,184.16 45.43,179.18 45.43,173.03V164.13C45.43,157.98 50.4,153 56.53,153H75.67V184.16Z"
android:fillType="evenOdd">
<aapt:attr name="android:fillColor">
<gradient
android:startX="45.43"
android:startY="132.25"
android:endX="87.19"
android:endY="221.04"
android:type="linear">
<item android:offset="0" android:color="#FF353DFF"/>
<item android:offset="1" android:color="#FF3DDCFF"/>
</gradient>
</aapt:attr>
</path>
</vector>

View File

@ -1,170 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

View File

@ -0,0 +1,49 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="278"
android:viewportHeight="278">
<group android:scaleX="0.75"
android:scaleY="0.75"
android:translateX="34.75"
android:translateY="34.75">
<path
android:pathData="M0.06,0.83h277v277h-277z"
android:fillColor="#111623"/>
<path
android:pathData="M108.01,80.13H174.31V95.81H108.01V80.13ZM96.91,95.81V80.13C96.91,73.98 101.88,69 108.01,69H174.31C180.44,69 185.41,73.98 185.41,80.13V95.81H189.78C198.97,95.81 206.43,103.29 206.43,112.5V189.31C206.43,198.53 198.97,206 189.78,206H92.32C83.12,206 75.67,198.53 75.67,189.31V184.16H127.18C133.31,184.16 138.28,179.18 138.28,173.03V164.13C138.28,157.98 133.31,153 127.18,153H75.67V112.5C75.67,103.29 83.12,95.81 92.32,95.81H96.91Z"
android:fillColor="#ffffff"
android:fillType="evenOdd"/>
<path
android:pathData="M75.67,184.16H56.53C50.4,184.16 45.43,179.18 45.43,173.03V164.13C45.43,157.98 50.4,153 56.53,153H75.67V184.16Z"
android:fillType="evenOdd">
<aapt:attr name="android:fillColor">
<gradient
android:startX="45.43"
android:startY="132.25"
android:endX="87.19"
android:endY="221.04"
android:type="linear">
<item android:offset="0" android:color="#FF353DFF"/>
<item android:offset="1" android:color="#FF3DDCFF"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M75.67,184.16H56.53C50.4,184.16 45.43,179.18 45.43,173.03V164.13C45.43,157.98 50.4,153 56.53,153H75.67V184.16Z"
android:fillType="evenOdd">
<aapt:attr name="android:fillColor">
<gradient
android:startX="45.43"
android:startY="132.25"
android:endX="87.19"
android:endY="221.04"
android:type="linear">
<item android:offset="0" android:color="#FF353DFF"/>
<item android:offset="1" android:color="#FF3DDCFF"/>
</gradient>
</aapt:attr>
</path>
</group>
</vector>

View File

@ -0,0 +1,44 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="278dp"
android:height="278dp"
android:viewportWidth="278"
android:viewportHeight="278">
<path
android:pathData="M0.06,0.83h277v277h-277z"
android:fillColor="#111623"/>
<path
android:pathData="M108.01,80.13H174.31V95.81H108.01V80.13ZM96.91,95.81V80.13C96.91,73.98 101.88,69 108.01,69H174.31C180.44,69 185.41,73.98 185.41,80.13V95.81H189.78C198.97,95.81 206.43,103.29 206.43,112.5V189.31C206.43,198.53 198.97,206 189.78,206H92.32C83.12,206 75.67,198.53 75.67,189.31V184.16H127.18C133.31,184.16 138.28,179.18 138.28,173.03V164.13C138.28,157.98 133.31,153 127.18,153H75.67V112.5C75.67,103.29 83.12,95.81 92.32,95.81H96.91Z"
android:fillColor="#ffffff"
android:fillType="evenOdd"/>
<path
android:pathData="M75.67,184.16H56.53C50.4,184.16 45.43,179.18 45.43,173.03V164.13C45.43,157.98 50.4,153 56.53,153H75.67V184.16Z"
android:fillType="evenOdd">
<aapt:attr name="android:fillColor">
<gradient
android:startX="45.43"
android:startY="132.25"
android:endX="87.19"
android:endY="221.04"
android:type="linear">
<item android:offset="0" android:color="#FF353DFF"/>
<item android:offset="1" android:color="#FF3DDCFF"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M75.67,184.16H56.53C50.4,184.16 45.43,179.18 45.43,173.03V164.13C45.43,157.98 50.4,153 56.53,153H75.67V184.16Z"
android:fillType="evenOdd">
<aapt:attr name="android:fillColor">
<gradient
android:startX="45.43"
android:startY="132.25"
android:endX="87.19"
android:endY="221.04"
android:type="linear">
<item android:offset="0" android:color="#FF353DFF"/>
<item android:offset="1" android:color="#FF3DDCFF"/>
</gradient>
</aapt:attr>
</path>
</vector>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 776 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#111623</color>
</resources>

View File

@ -13,4 +13,66 @@
<string name="updates_available">Available updates</string>
<string name="expand">Expand</string>
<string name="update">Update</string>
<string name="app_selector_title">Select an app…</string>
<string name="app_bar_open_discord">Discord</string>
<string name="app_bar_open_github">GitHub</string>
<string name="card_announcement_header">Announcement</string>
<string name="card_commits_header">Latest updates</string>
<string name="card_logs_header">Logs</string>
<string name="card_application_header">Select Application</string>
<string name="card_patches_header">Select Patches</string>
<string name="card_options_header">Options</string>
<string name="card_contributors_header">Contributors</string>
<string name="card_commits_body_patcher">Patcher:</string>
<string name="card_commits_body_manager">Manager:</string>
<string name="card_announcement_body_placeholder">This is an example text for previewing ReVanced Manager design that will be replaced by dynamic announcements in the future.</string>
<string name="card_options_body_root">Root</string>
<string name="card_options_body_use_installed">Use installed YouTube app</string>
<string name="card_application_not_loaded">Loading applications.</string>
<string name="card_application_not_selected">No application selected.</string>
<string name="card_patches_body_source">No patches source selected.</string>
<string name="card_patches_body_patches">No patches selected.</string>
<string name="card_application_body_selected">Selected:</string>
<string name="card_logs_body">Click here to view patcher logs.</string>
<string name="card_credits_body">Click here to view people who have contributed to the ReVanced Project.</string>
<string name="loading_body">One moment, please…</string>
<string name="loading_fetching_patches">Fetching patches</string>
<string name="unsupported">Unsupported version</string>
<string name="only_compatible">This patch is only compatible with version:</string>
<string name="card_announcement_button_changelog">Changelog</string>
<string name="button_patch">Patch</string>
<string name="navigation_dashboard">Dashboard</string>
<string name="navigation_patcher">Patcher</string>
<string name="screen_logs_title">Logs</string>
<string name="screen_contributors_title">Contributors</string>
<string name="screen_credits_team_manager">ReVanced Manager</string>
<string name="screen_credits_team_website">Website</string>
<string name="screen_credits_team">Team</string>
<string name="screen_credits_translators">Translators</string>
<string name="screen_credits_team_patcher">Patcher</string>
<string name="help">Help</string>
<string name="help_translate">Help translate</string>
<string name="whats_new">What\'s New</string>
<string name="ic_non_selected">No patches are selected!</string>
<string name="patcher_contributors">Patcher Contributors</string>
<string name="contributor_image">Contributor image</string>
<string name="no_contributors">No contributors</string>
<string name="screen_settings_title">Settings</string>
<string name="screen_about_title">About</string>
<string name="about">About</string>
<string name="navigation_more">More</string>
<string name="cli_contributors">CLI Contributors</string>
<string name="patches_contributors">Patches Contributors</string>
<string name="manager_contributors">Manager Contributors</string>
<string name="integrations_contributors">Integrations Contributors</string>
<string name="dropdown_button">Dropdown Button</string>
<string name="app_version">Version</string>
<string name="faq">FAQ</string>
<string name="version_info">Version info</string>
<string name="unsupported_version">Unsupported version</string>
<string name="compatible_versions">Compatible app versions</string>
<string name="patcher_notification_title">Patching</string>
<string name="patcher_notification_message">ReVanced Manager is patching</string>
<string name="theme">Theme</string>
<string name="apply">Apply</string>
</resources>

View File

@ -1,5 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.ReVancedManager" parent="android:Theme.Material.Light.NoActionBar" />
<style name="Theme.ReVancedManager" parent="Theme.SplashScreen">
<item name="windowSplashScreenAnimatedIcon">@drawable/ic_launcher_foreground</item>
<item name="postSplashScreenTheme">@style/Theme.AppCompat.NoActionBar</item>
</style>
</resources>

View File

@ -1,22 +1,21 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
extra.apply {
// Global variable for some dependencies
set("compose_version", "1.3.0-rc02")
set("ktor_version", "2.0.1")
set("room_version", "2.4.2")
}
repositories {
google()
mavenCentral()
}
dependencies {
classpath("com.android.tools.build:gradle:7.4.0-alpha10")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.0")
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle.kts files
}
}
allprojects {
repositories {
google()
mavenCentral()
maven("https://jitpack.io")
}
plugins {
id("com.android.application") version "7.4.0-alpha10" apply false
id("com.android.library") version "7.4.0-alpha10" apply false
id("org.jetbrains.kotlin.android") version "1.7.10" apply false
id("com.google.devtools.ksp") version "1.7.10-+" apply false
}
repositories {
google()
}

View File

@ -1,2 +1,15 @@
pluginManagement {
repositories {
gradlePluginPortal()
google()
mavenCentral()
}
}
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
}
}
rootProject.name = "ReVanced Manager"
include(":app")