feat: patcher
Co-authored-by: Canny <canny1913@outlook.com>
11
.github/dependabot.yml
vendored
Normal 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
@ -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
@ -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
|
@ -1,18 +1,37 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id("com.android.application")
|
id("com.android.application")
|
||||||
id("kotlin-android")
|
id("org.jetbrains.kotlin.android")
|
||||||
|
id("com.google.devtools.ksp")
|
||||||
id("kotlin-parcelize")
|
id("kotlin-parcelize")
|
||||||
kotlin("plugin.serialization") version "1.7.10"
|
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 {
|
android {
|
||||||
namespace = "app.revanced.manager"
|
namespace = "app.revanced.manager"
|
||||||
compileSdk = 32
|
compileSdk = 33
|
||||||
|
|
||||||
|
lint {
|
||||||
|
abortOnError = false
|
||||||
|
disable += "DialogFragmentCallbacksDetector"
|
||||||
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "app.revanced.manager.compose"
|
applicationId = "app.revanced.manager.compose"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 32
|
targetSdk = 33
|
||||||
versionCode = 1
|
versionCode = 1
|
||||||
versionName = "0.0.1"
|
versionName = "0.0.1"
|
||||||
|
|
||||||
@ -22,7 +41,11 @@ android {
|
|||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
isMinifyEnabled = true
|
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"
|
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
|
buildFeatures.compose = true
|
||||||
composeOptions.kotlinCompilerExtensionVersion = "1.2.0"
|
composeOptions.kotlinCompilerExtensionVersion = "1.3.0-rc02"
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
// AndroidX core
|
// 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")
|
implementation("androidx.core:core-splashscreen:1.0.0")
|
||||||
|
|
||||||
// AndroidX activity
|
// 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
|
// Koin
|
||||||
val koinVersion = "3.2.0"
|
val koinVersion = "3.2.0"
|
||||||
@ -54,16 +88,17 @@ dependencies {
|
|||||||
implementation("io.insert-koin:koin-androidx-compose:$koinVersion")
|
implementation("io.insert-koin:koin-androidx-compose:$koinVersion")
|
||||||
|
|
||||||
// Compose
|
// Compose
|
||||||
val composeVersion = "1.3.0-alpha01"
|
val composeVersion = "1.3.0-alpha03"
|
||||||
implementation("androidx.compose.ui:ui:${composeVersion}")
|
implementation("androidx.compose.ui:ui:${composeVersion}")
|
||||||
debugImplementation("androidx.compose.ui:ui-tooling:${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}")
|
implementation("androidx.compose.material:material-icons-extended:${composeVersion}")
|
||||||
|
|
||||||
// Accompanist
|
// Accompanist
|
||||||
val accompanistVersion = "0.26.0-alpha"
|
val accompanistVersion = "0.26.0-alpha"
|
||||||
implementation("com.google.accompanist:accompanist-systemuicontroller:$accompanistVersion")
|
implementation("com.google.accompanist:accompanist-systemuicontroller:$accompanistVersion")
|
||||||
implementation("com.google.accompanist:accompanist-placeholder-material:$accompanistVersion")
|
implementation("com.google.accompanist:accompanist-placeholder-material:$accompanistVersion")
|
||||||
|
implementation("com.google.accompanist:accompanist-drawablepainter:$accompanistVersion")
|
||||||
|
|
||||||
// Coil (async image loading)
|
// Coil (async image loading)
|
||||||
implementation("io.coil-kt:coil-compose:2.1.0")
|
implementation("io.coil-kt:coil-compose:2.1.0")
|
||||||
@ -74,10 +109,20 @@ dependencies {
|
|||||||
// Taxi (navigation)
|
// Taxi (navigation)
|
||||||
implementation("com.github.X1nto:Taxi:1.2.0")
|
implementation("com.github.X1nto:Taxi:1.2.0")
|
||||||
|
|
||||||
// Ktor
|
|
||||||
val ktorVersion = "2.0.3"
|
val ktorVersion = "2.0.3"
|
||||||
implementation("io.ktor:ktor-client-core:$ktorVersion")
|
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-cio:$ktorVersion")
|
||||||
implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion")
|
implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion")
|
||||||
implementation("io.ktor:ktor-serialization-kotlinx-json:$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")
|
||||||
|
}
|
||||||
|
41
app/proguard-rules.pro
vendored
@ -14,8 +14,41 @@
|
|||||||
|
|
||||||
# Uncomment this to preserve the line number information for
|
# Uncomment this to preserve the line number information for
|
||||||
# debugging stack traces.
|
# 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
|
# These classes required for the patcher to function correctly.
|
||||||
# hide the original source file name.
|
-keep class app.revanced.patcher.** {
|
||||||
#-renamesourcefileattribute SourceFile
|
*;
|
||||||
|
}
|
||||||
|
-keep class brut.** {
|
||||||
|
*;
|
||||||
|
}
|
||||||
|
-keep class org.xmlpull.** {
|
||||||
|
*;
|
||||||
|
}
|
||||||
|
-keep class kotlin.** {
|
||||||
|
*;
|
||||||
|
}
|
||||||
|
-keep class org.jf.** {
|
||||||
|
*;
|
||||||
|
}
|
||||||
|
|
||||||
|
-keepattributes RuntimeVisibleAnnotations,AnnotationDefault
|
||||||
|
@ -2,26 +2,46 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
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.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
|
<application
|
||||||
android:name=".ManagerApplication"
|
android:name=".ManagerApplication"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
android:extractNativeLibs="true"
|
||||||
android:fullBackupContent="@xml/backup_rules"
|
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
|
android:largeHeap="true"
|
||||||
android:theme="@style/Theme.ReVancedManager"
|
android:theme="@style/Theme.ReVancedManager"
|
||||||
tools:targetApi="31">
|
|
||||||
|
tools:targetApi="33">
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:theme="@style/Theme.ReVancedManager">
|
android:theme="@style/Theme.ReVancedManager">
|
||||||
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
BIN
app/src/main/ic_launcher-playstore.png
Normal file
After Width: | Height: | Size: 9.4 KiB |
@ -8,12 +8,17 @@ import androidx.compose.animation.ExperimentalAnimationApi
|
|||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.animation.with
|
import androidx.compose.animation.with
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||||
import app.revanced.manager.preferences.PreferencesManager
|
import app.revanced.manager.preferences.PreferencesManager
|
||||||
import app.revanced.manager.ui.navigation.AppDestination
|
import app.revanced.manager.ui.navigation.AppDestination
|
||||||
import app.revanced.manager.ui.screen.MainDashboardScreen
|
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.ReVancedManagerTheme
|
||||||
|
import app.revanced.manager.ui.theme.Theme
|
||||||
import com.xinto.taxi.Taxi
|
import com.xinto.taxi.Taxi
|
||||||
import com.xinto.taxi.rememberBackstackNavigator
|
import com.xinto.taxi.rememberBackstackNavigator
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
@ -23,9 +28,13 @@ class MainActivity : ComponentActivity() {
|
|||||||
|
|
||||||
@OptIn(ExperimentalAnimationApi::class)
|
@OptIn(ExperimentalAnimationApi::class)
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
installSplashScreen()
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContent {
|
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)
|
val navigator = rememberBackstackNavigator<AppDestination>(AppDestination.Dashboard)
|
||||||
|
|
||||||
BackHandler {
|
BackHandler {
|
||||||
@ -39,6 +48,10 @@ class MainActivity : ComponentActivity() {
|
|||||||
) { destination ->
|
) { destination ->
|
||||||
when (destination) {
|
when (destination) {
|
||||||
is AppDestination.Dashboard -> MainDashboardScreen(navigator = navigator)
|
is AppDestination.Dashboard -> MainDashboardScreen(navigator = navigator)
|
||||||
|
is AppDestination.AppSelector -> AppSelectorSubscreen(
|
||||||
|
navigator = navigator
|
||||||
|
)
|
||||||
|
is AppDestination.PatchSelector -> PatchesSelectorSubscreen(navigator = navigator)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
19
app/src/main/java/app/revanced/manager/Variables.kt
Normal 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>()
|
||||||
|
}
|
90
app/src/main/java/app/revanced/manager/api/API.kt
Normal 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()
|
@ -1,9 +1,11 @@
|
|||||||
package app.revanced.manager.di
|
package app.revanced.manager.di
|
||||||
|
|
||||||
|
import app.revanced.manager.api.API
|
||||||
import app.revanced.manager.repository.GitHubRepository
|
import app.revanced.manager.repository.GitHubRepository
|
||||||
import org.koin.core.module.dsl.singleOf
|
import org.koin.core.module.dsl.singleOf
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
|
|
||||||
val repositoryModule = module {
|
val repositoryModule = module {
|
||||||
singleOf(::GitHubRepository)
|
singleOf(::GitHubRepository)
|
||||||
|
singleOf(::API)
|
||||||
}
|
}
|
@ -1,10 +1,15 @@
|
|||||||
package app.revanced.manager.di
|
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.androidx.viewmodel.dsl.viewModelOf
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
|
|
||||||
val viewModelModule = module {
|
val viewModelModule = module {
|
||||||
viewModelOf(::SettingsViewModel)
|
viewModelOf(::SettingsViewModel)
|
||||||
viewModelOf(::DashboardViewModel)
|
viewModelOf(::DashboardViewModel)
|
||||||
|
viewModelOf(::PatcherViewModel)
|
||||||
|
viewModelOf(::AppSelectorViewModel)
|
||||||
}
|
}
|
@ -3,7 +3,7 @@ package app.revanced.manager.dto.github
|
|||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class ApiCommit(
|
class APICommit(
|
||||||
val sha: String,
|
val sha: String,
|
||||||
val commit: Object
|
val commit: Object
|
||||||
) {
|
) {
|
||||||
|
@ -4,7 +4,7 @@ import kotlinx.serialization.SerialName
|
|||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class ApiContributor(
|
class APIContributor(
|
||||||
@SerialName("login") val username: String,
|
@SerialName("login") val username: String,
|
||||||
@SerialName("avatar_url") val avatarUrl: String,
|
@SerialName("avatar_url") val avatarUrl: String,
|
||||||
@SerialName("html_url") val profileUrl: String,
|
@SerialName("html_url") val profileUrl: String,
|
||||||
|
@ -4,7 +4,7 @@ import kotlinx.serialization.SerialName
|
|||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class ApiRelease(
|
data class APIRelease(
|
||||||
@SerialName("tag_name") val tagName: String,
|
@SerialName("tag_name") val tagName: String,
|
||||||
@SerialName("published_at") val publishedAt: String,
|
@SerialName("published_at") val publishedAt: String,
|
||||||
val prerelease: Boolean,
|
val prerelease: Boolean,
|
||||||
|
12
app/src/main/java/app/revanced/manager/patcher/aapt/Aapt.kt
Normal 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())
|
@ -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
|
||||||
|
}
|
@ -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())
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
@ -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 } }
|
||||||
|
}
|
||||||
|
}
|
@ -5,6 +5,7 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
|
import app.revanced.manager.ui.theme.Theme
|
||||||
import app.revanced.manager.util.ghIntegrations
|
import app.revanced.manager.util.ghIntegrations
|
||||||
import app.revanced.manager.util.ghPatches
|
import app.revanced.manager.util.ghPatches
|
||||||
import kotlin.reflect.KProperty
|
import kotlin.reflect.KProperty
|
||||||
@ -13,7 +14,7 @@ class PreferencesManager(
|
|||||||
sharedPreferences: SharedPreferences
|
sharedPreferences: SharedPreferences
|
||||||
) : BasePreferenceManager(sharedPreferences) {
|
) : BasePreferenceManager(sharedPreferences) {
|
||||||
var dynamicColor by booleanPreference("dynamic_color", true)
|
var dynamicColor by booleanPreference("dynamic_color", true)
|
||||||
|
var theme by enumPreference("theme", Theme.SYSTEM)
|
||||||
var srcPatches by stringPreference("src_patches", ghPatches)
|
var srcPatches by stringPreference("src_patches", ghPatches)
|
||||||
var srcIntegrations by stringPreference("src_integrations", ghIntegrations)
|
var srcIntegrations by stringPreference("src_integrations", ghIntegrations)
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
package app.revanced.manager.repository
|
package app.revanced.manager.repository
|
||||||
|
|
||||||
import app.revanced.manager.dto.github.ApiCommit
|
import app.revanced.manager.dto.github.APICommit
|
||||||
import app.revanced.manager.dto.github.ApiContributor
|
import app.revanced.manager.dto.github.APIContributor
|
||||||
import app.revanced.manager.dto.github.ApiRelease
|
import app.revanced.manager.dto.github.APIRelease
|
||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
import io.ktor.client.call.*
|
import io.ktor.client.call.*
|
||||||
import io.ktor.client.request.*
|
import io.ktor.client.request.*
|
||||||
@ -11,20 +11,19 @@ import kotlinx.coroutines.withContext
|
|||||||
|
|
||||||
class GitHubRepository(private val client: HttpClient) {
|
class GitHubRepository(private val client: HttpClient) {
|
||||||
suspend fun getLatestRelease(repo: String) = withContext(Dispatchers.IO) {
|
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)
|
parameter("per_page", 1)
|
||||||
}.body()
|
}.body()
|
||||||
res.first()
|
res.first()
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getLatestCommit(repo: String, ref: String) = withContext(Dispatchers.IO) {
|
suspend fun getLatestCommit(repo: String, ref: String) = withContext(Dispatchers.IO) {
|
||||||
client.get("$baseUrl/$repo/commits/$ref") {
|
client.get("$baseUrl/$repo/commits/$ref") {
|
||||||
parameter("per_page", 1)
|
parameter("per_page", 1)
|
||||||
}.body() as ApiCommit
|
}.body() as APICommit
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getContributors(org: String, repo: String) = withContext(Dispatchers.IO) {
|
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 {
|
private companion object {
|
||||||
|
10
app/src/main/java/app/revanced/manager/ui/Resource.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
)
|
||||||
|
}
|
@ -1,16 +1,12 @@
|
|||||||
package app.revanced.manager.ui.component
|
package app.revanced.manager.ui.component
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedButton
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import app.revanced.manager.R
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ApplicationItem(
|
fun ApplicationItem(
|
||||||
|
@ -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)
|
||||||
|
}
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
@ -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")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
@ -13,7 +13,6 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
import app.revanced.manager.util.ghOrganization
|
import app.revanced.manager.util.ghOrganization
|
||||||
import app.revanced.manager.util.openUrl
|
import app.revanced.manager.util.openUrl
|
||||||
@ -34,7 +33,6 @@ fun SocialItem(@StringRes label: Int, vec: ImageVector, fn: () -> Unit) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SocialItemPreview() {
|
fun SocialItemPreview() {
|
||||||
val ctx = LocalContext.current.applicationContext
|
val ctx = LocalContext.current.applicationContext
|
||||||
|
@ -15,6 +15,12 @@ import kotlinx.parcelize.RawValue
|
|||||||
sealed interface AppDestination : Destination {
|
sealed interface AppDestination : Destination {
|
||||||
@Parcelize
|
@Parcelize
|
||||||
object Dashboard : AppDestination
|
object Dashboard : AppDestination
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
object AppSelector : AppDestination
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
object PatchSelector : AppDestination
|
||||||
}
|
}
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
|
@ -91,46 +91,24 @@ fun DashboardScreen(viewModel: DashboardViewModel = getViewModel()) {
|
|||||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
) {
|
) {
|
||||||
ApplicationItem(
|
ApplicationItem(
|
||||||
name = "ReVanced",
|
name = "Youtube ReVanced",
|
||||||
released = "com.google.android.youtube",
|
released = "Released [who knows] centuries ago",
|
||||||
icon = { Icon(Icons.Default.Dashboard, "ReVanced") }
|
icon = { Icon(Icons.Default.Dashboard, "ReVanced") }
|
||||||
) {
|
) {
|
||||||
ChangelogText(
|
ChangelogText(
|
||||||
"""
|
"""
|
||||||
fix: aaaaaa
|
cossal will explode
|
||||||
fix: aaaaaa
|
|
||||||
fix: aaaaaa
|
|
||||||
fix: aaaaaa
|
|
||||||
fix: aaaaaa
|
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
ApplicationItem(
|
ApplicationItem(
|
||||||
name = "ReReddit",
|
name = "Reddit ReVanced",
|
||||||
released = "Released 1 month ago",
|
released = "Released [REDACTED] month ago",
|
||||||
icon = { Icon(Icons.Default.Build, "ReReddit") }
|
icon = { Icon(Icons.Default.Build, "ReReddit") }
|
||||||
) {
|
) {
|
||||||
ChangelogText(
|
ChangelogText(
|
||||||
"""
|
"""
|
||||||
fix: bbbbbb
|
hi ushie
|
||||||
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()
|
""".trimIndent()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
package app.revanced.manager.ui.screen
|
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.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
@ -19,8 +22,8 @@ import com.xinto.taxi.rememberNavigator
|
|||||||
@Composable
|
@Composable
|
||||||
fun MainDashboardScreen(navigator: BackstackNavigator<AppDestination>) {
|
fun MainDashboardScreen(navigator: BackstackNavigator<AppDestination>) {
|
||||||
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(
|
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(
|
||||||
decayAnimationSpec = rememberSplineBasedDecay(),
|
state = rememberTopAppBarState(),
|
||||||
state = rememberTopAppBarState()
|
canScroll = { true }
|
||||||
)
|
)
|
||||||
|
|
||||||
val mainRootNavigator = rememberNavigator(DashboardDestination.DASHBOARD)
|
val mainRootNavigator = rememberNavigator(DashboardDestination.DASHBOARD)
|
||||||
@ -31,7 +34,7 @@ fun MainDashboardScreen(navigator: BackstackNavigator<AppDestination>) {
|
|||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||||
topBar = {
|
topBar = {
|
||||||
MediumTopAppBar(
|
LargeTopAppBar(
|
||||||
title = {
|
title = {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(mainRootNavigator.currentDestination.label),
|
text = stringResource(mainRootNavigator.currentDestination.label),
|
||||||
@ -64,7 +67,10 @@ fun MainDashboardScreen(navigator: BackstackNavigator<AppDestination>) {
|
|||||||
) { destination ->
|
) { destination ->
|
||||||
when (destination) {
|
when (destination) {
|
||||||
DashboardDestination.DASHBOARD -> DashboardScreen()
|
DashboardDestination.DASHBOARD -> DashboardScreen()
|
||||||
DashboardDestination.PATCHER -> PatcherScreen()
|
DashboardDestination.PATCHER -> PatcherScreen(
|
||||||
|
onClickAppSelector = { navigator.push(AppDestination.AppSelector) },
|
||||||
|
onClickPatchSelector = { navigator.push(AppDestination.PatchSelector) }
|
||||||
|
)
|
||||||
DashboardDestination.SETTINGS -> SettingsScreen()
|
DashboardDestination.SETTINGS -> SettingsScreen()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,155 +1,96 @@
|
|||||||
package app.revanced.manager.ui.screen
|
package app.revanced.manager.ui.screen
|
||||||
|
|
||||||
import android.widget.Toast
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.annotation.StringRes
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.verticalScroll
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Build
|
import androidx.compose.material.icons.filled.Build
|
||||||
import androidx.compose.material.icons.filled.Dashboard
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.material3.Button
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
import app.revanced.manager.ui.component.ApplicationItem
|
import app.revanced.manager.Variables.patches
|
||||||
import app.revanced.manager.ui.component.HeadlineWithCard
|
import app.revanced.manager.Variables.selectedAppPackage
|
||||||
import app.revanced.manager.ui.viewmodel.DashboardViewModel
|
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 app.revanced.manager.ui.viewmodel.PatcherViewModel
|
||||||
import org.koin.androidx.compose.getViewModel
|
import org.koin.androidx.compose.getViewModel
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun PatcherScreen(viewModel: DashboardViewModel = getViewModel()) {
|
fun PatcherScreen(
|
||||||
val context = LocalContext.current
|
onClickAppSelector: () -> Unit,
|
||||||
val padHoriz = 16.dp
|
onClickPatchSelector: () -> Unit,
|
||||||
val padVert = 10.dp
|
viewModel: PatcherViewModel = getViewModel()
|
||||||
|
) {
|
||||||
|
val selectedAmount = selectedPatches.size
|
||||||
|
val selectedAppPackage by selectedAppPackage
|
||||||
|
val hasAppSelected = selectedAppPackage.isPresent
|
||||||
|
val patchesLoaded = patches.value is Resource.Success
|
||||||
|
|
||||||
Column(
|
Scaffold(floatingActionButton = {
|
||||||
modifier = Modifier
|
FloatingActionButton(
|
||||||
.fillMaxSize()
|
enabled = hasAppSelected && viewModel.anyPatchSelected(),
|
||||||
.padding(horizontal = 18.dp)
|
onClick = { viewModel.startPatcher() },
|
||||||
.verticalScroll(state = rememberScrollState()),
|
icon = { Icon(Icons.Default.Build, contentDescription = "Patch") },
|
||||||
horizontalAlignment = Alignment.Start,
|
text = { Text(text = "Patch") }
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
)
|
||||||
) {
|
}) { paddingValues ->
|
||||||
HeadlineWithCard(R.string.updates) {
|
Column(
|
||||||
Row(
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(paddingValues)
|
||||||
|
.padding(16.dp),
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(horizontal = padHoriz, vertical = padVert)
|
.padding(4.dp)
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
enabled = patchesLoaded,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
onClick = onClickAppSelector
|
||||||
) {
|
) {
|
||||||
Column {
|
Column(modifier = Modifier.padding(16.dp)) {
|
||||||
CommitDate(
|
Text(
|
||||||
label = R.string.patcher,
|
text = stringResource(id = R.string.card_application_header),
|
||||||
date = viewModel.patcherCommitDate
|
style = MaterialTheme.typography.titleMedium
|
||||||
)
|
)
|
||||||
CommitDate(
|
Text(
|
||||||
label = R.string.manager,
|
text = if (patchesLoaded) {
|
||||||
date = viewModel.managerCommitDate
|
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(
|
Card(
|
||||||
enabled = false, // needs update
|
modifier = Modifier
|
||||||
onClick = {
|
.padding(4.dp)
|
||||||
Toast.makeText(context, "Already up-to-date!", Toast.LENGTH_SHORT)
|
.fillMaxWidth(),
|
||||||
.show()
|
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)) }
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
Text(
|
modifier = Modifier.padding(0.dp, 8.dp)
|
||||||
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()
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,26 +1,27 @@
|
|||||||
package app.revanced.manager.ui.screen
|
package app.revanced.manager.ui.screen
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Code
|
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.material3.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import app.revanced.manager.R
|
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.component.SocialItem
|
||||||
import app.revanced.manager.ui.navigation.AppDestination
|
import app.revanced.manager.ui.theme.Theme
|
||||||
import app.revanced.manager.ui.viewmodel.SettingsViewModel
|
import app.revanced.manager.ui.viewmodel.SettingsViewModel
|
||||||
import com.xinto.taxi.BackstackNavigator
|
import org.koin.androidx.compose.get
|
||||||
import org.koin.androidx.compose.getViewModel
|
import org.koin.androidx.compose.getViewModel
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@ -35,18 +36,82 @@ fun SettingsScreen(viewModel: SettingsViewModel = getViewModel()) {
|
|||||||
.verticalScroll(state = rememberScrollState()),
|
.verticalScroll(state = rememberScrollState()),
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
) {
|
) {
|
||||||
|
if (viewModel.showThemePicker) {
|
||||||
|
ThemePicker(
|
||||||
|
onDismissRequest = viewModel::dismissThemePicker,
|
||||||
|
onConfirm = viewModel::setTheme
|
||||||
|
)
|
||||||
|
}
|
||||||
|
GroupHeader(title = "Appearance")
|
||||||
ListItem(
|
ListItem(
|
||||||
modifier = Modifier.clickable { prefs.dynamicColor = !prefs.dynamicColor },
|
modifier = Modifier.clickable { viewModel.showThemePicker() },
|
||||||
headlineText = { Text(stringResource(R.string.dynamic_color)) },
|
headlineText = { Text(stringResource(R.string.theme)) },
|
||||||
trailingContent = {
|
leadingContent = { Icon(Icons.Default.Style, contentDescription = null) },
|
||||||
Switch(
|
trailingContent = { FilledTonalButton(onClick = { viewModel.showThemePicker() }) {
|
||||||
checked = prefs.dynamicColor,
|
Text(text = prefs.theme.displayName)
|
||||||
onCheckedChange = { prefs.dynamicColor = it }
|
} }
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
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()
|
Divider()
|
||||||
SocialItem(R.string.github, Icons.Default.Code, viewModel::openGitHub)
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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)) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -48,4 +48,9 @@ fun ReVancedManagerTheme(
|
|||||||
typography = Typography,
|
typography = Typography,
|
||||||
content = content
|
content = content
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
enum class Theme(val displayName: String) {
|
||||||
|
SYSTEM("System"),
|
||||||
|
LIGHT("Light"),
|
||||||
|
DARK("Dark");
|
||||||
}
|
}
|
@ -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)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -1,40 +1,164 @@
|
|||||||
package app.revanced.manager.ui.viewmodel
|
package app.revanced.manager.ui.viewmodel
|
||||||
|
|
||||||
import android.text.format.DateUtils
|
import android.app.Application
|
||||||
import androidx.compose.runtime.getValue
|
import android.content.pm.PackageManager
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import android.os.Parcelable
|
||||||
import androidx.compose.runtime.setValue
|
import android.util.Log
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import app.revanced.manager.repository.GitHubRepository
|
import androidx.lifecycle.viewModelScope
|
||||||
import app.revanced.manager.util.ghManager
|
import androidx.work.ExistingWorkPolicy
|
||||||
import app.revanced.manager.util.ghPatcher
|
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 kotlinx.coroutines.runBlocking
|
||||||
import java.text.SimpleDateFormat
|
import kotlinx.parcelize.Parcelize
|
||||||
import java.util.*
|
import java.io.File
|
||||||
|
|
||||||
class PatcherViewModel(private val repository: GitHubRepository) : ViewModel() {
|
class PatcherViewModel(private val app: Application, private val api: API) : ViewModel() {
|
||||||
var patcherCommitDate by mutableStateOf("")
|
private val workdir = createWorkDir()
|
||||||
private set
|
private lateinit var patchBundleFile: String
|
||||||
var managerCommitDate by mutableStateOf("")
|
private val tag = "ReVanced Manager"
|
||||||
private set
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
runBlocking {
|
runBlocking {
|
||||||
patcherCommitDate = commitDateOf(ghPatcher)
|
loadPatches()
|
||||||
managerCommitDate = commitDateOf(ghManager)
|
downloadIntegrations()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun commitDateOf(repo: String, ref: String = "HEAD"): String {
|
fun selectPatch(patchId: String, state: Boolean) {
|
||||||
val commit = repository.getLatestCommit(repo, ref).commit
|
if (state) selectedPatches.add(patchId)
|
||||||
return DateUtils.getRelativeTimeSpanString(
|
else selectedPatches.remove(patchId)
|
||||||
formatter.parse(commit.committer.date)!!.time,
|
|
||||||
Calendar.getInstance().timeInMillis,
|
|
||||||
DateUtils.MINUTE_IN_MILLIS
|
|
||||||
).toString()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private companion object {
|
private suspend fun downloadIntegrations() {
|
||||||
val formatter = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.getDefault())
|
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
|
@ -1,8 +1,12 @@
|
|||||||
package app.revanced.manager.ui.viewmodel
|
package app.revanced.manager.ui.viewmodel
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import app.revanced.manager.preferences.PreferencesManager
|
import app.revanced.manager.preferences.PreferencesManager
|
||||||
|
import app.revanced.manager.ui.theme.Theme
|
||||||
import app.revanced.manager.util.ghOrganization
|
import app.revanced.manager.util.ghOrganization
|
||||||
import app.revanced.manager.util.openUrl
|
import app.revanced.manager.util.openUrl
|
||||||
|
|
||||||
@ -10,5 +14,19 @@ class SettingsViewModel(
|
|||||||
private val app: Application,
|
private val app: Application,
|
||||||
val prefs: PreferencesManager
|
val prefs: PreferencesManager
|
||||||
) : ViewModel() {
|
) : 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)
|
fun openGitHub() = app.openUrl(ghOrganization)
|
||||||
}
|
}
|
BIN
app/src/main/jniLibs/arm64-v8a/libaapt2.so
Normal file
BIN
app/src/main/jniLibs/armeabi-v7a/libaapt2.so
Normal file
BIN
app/src/main/jniLibs/x86/libaapt2.so
Normal file
@ -2,29 +2,48 @@
|
|||||||
xmlns:aapt="http://schemas.android.com/aapt"
|
xmlns:aapt="http://schemas.android.com/aapt"
|
||||||
android:width="108dp"
|
android:width="108dp"
|
||||||
android:height="108dp"
|
android:height="108dp"
|
||||||
android:viewportWidth="108"
|
android:viewportWidth="278"
|
||||||
android:viewportHeight="108">
|
android:viewportHeight="278">
|
||||||
<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">
|
<group android:scaleX="0.75"
|
||||||
<aapt:attr name="android:fillColor">
|
android:scaleY="0.75"
|
||||||
<gradient
|
android:translateX="34.75"
|
||||||
android:endX="85.84757"
|
android:translateY="34.75">
|
||||||
android:endY="92.4963"
|
<path
|
||||||
android:startX="42.9492"
|
android:pathData="M0.06,0.83h277v277h-277z"
|
||||||
android:startY="49.59793"
|
android:fillColor="#111623"/>
|
||||||
android:type="linear">
|
<path
|
||||||
<item
|
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:color="#44000000"
|
android:fillColor="#ffffff"
|
||||||
android:offset="0.0" />
|
android:fillType="evenOdd"/>
|
||||||
<item
|
<path
|
||||||
android:color="#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:offset="1.0" />
|
android:fillType="evenOdd">
|
||||||
</gradient>
|
<aapt:attr name="android:fillColor">
|
||||||
</aapt:attr>
|
<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>
|
||||||
<path
|
<path
|
||||||
android:fillColor="#FFFFFF"
|
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="nonZero"
|
android:fillType="evenOdd">
|
||||||
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"
|
<aapt:attr name="android:fillColor">
|
||||||
android:strokeWidth="1"
|
<gradient
|
||||||
android:strokeColor="#00000000" />
|
android:startX="45.43"
|
||||||
</vector>
|
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>
|
||||||
|
44
app/src/main/res/drawable-v24/manager.xml
Normal 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>
|
@ -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>
|
|
49
app/src/main/res/drawable/ic_launcher_foreground.xml
Normal 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>
|
44
app/src/main/res/drawable/manager.xml
Normal 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>
|
@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@drawable/ic_launcher_background" />
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@drawable/ic_launcher_background" />
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
BIN
app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
Before Width: | Height: | Size: 1.4 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 2.8 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 776 B |
Before Width: | Height: | Size: 982 B |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 1.7 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.9 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 3.8 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 2.8 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 6.9 KiB |
Before Width: | Height: | Size: 5.8 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 3.8 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 9.8 KiB |
Before Width: | Height: | Size: 7.6 KiB |
4
app/src/main/res/values/ic_launcher_background.xml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="ic_launcher_background">#111623</color>
|
||||||
|
</resources>
|
@ -13,4 +13,66 @@
|
|||||||
<string name="updates_available">Available updates</string>
|
<string name="updates_available">Available updates</string>
|
||||||
<string name="expand">Expand</string>
|
<string name="expand">Expand</string>
|
||||||
<string name="update">Update</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>
|
</resources>
|
@ -1,5 +1,8 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<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>
|
</resources>
|
@ -1,22 +1,21 @@
|
|||||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
|
||||||
buildscript {
|
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 {
|
repositories {
|
||||||
google()
|
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 {
|
plugins {
|
||||||
repositories {
|
id("com.android.application") version "7.4.0-alpha10" apply false
|
||||||
google()
|
id("com.android.library") version "7.4.0-alpha10" apply false
|
||||||
mavenCentral()
|
id("org.jetbrains.kotlin.android") version "1.7.10" apply false
|
||||||
maven("https://jitpack.io")
|
id("com.google.devtools.ksp") version "1.7.10-+" apply false
|
||||||
}
|
}
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
}
|
}
|
@ -20,4 +20,4 @@ kotlin.code.style=official
|
|||||||
# Enables namespacing of each library's R class so that its R class includes only the
|
# Enables namespacing of each library's R class so that its R class includes only the
|
||||||
# resources declared in the library itself and none from the library's dependencies,
|
# resources declared in the library itself and none from the library's dependencies,
|
||||||
# thereby reducing the size of the R class for that library
|
# thereby reducing the size of the R class for that library
|
||||||
android.nonTransitiveRClass=true
|
android.nonTransitiveRClass=true
|
||||||
|
@ -1,2 +1,15 @@
|
|||||||
|
pluginManagement {
|
||||||
|
repositories {
|
||||||
|
gradlePluginPortal()
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dependencyResolutionManagement {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
rootProject.name = "ReVanced Manager"
|
rootProject.name = "ReVanced Manager"
|
||||||
include(":app")
|
include(":app")
|