Initial ui and functionality
This commit is contained in:
parent
82ce3d2d48
commit
92a650b067
|
@ -0,0 +1 @@
|
||||||
|
Vendetta Manager
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="CompilerConfiguration">
|
||||||
|
<bytecodeTargetLevel target="17" />
|
||||||
|
</component>
|
||||||
|
</project>
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="DiscordProjectSettings">
|
||||||
|
<option name="show" value="ASK" />
|
||||||
|
<option name="description" value="" />
|
||||||
|
</component>
|
||||||
|
</project>
|
|
@ -0,0 +1,35 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="RemoteRepositoriesConfiguration">
|
||||||
|
<remote-repository>
|
||||||
|
<option name="id" value="central" />
|
||||||
|
<option name="name" value="Maven Central repository" />
|
||||||
|
<option name="url" value="https://repo1.maven.org/maven2" />
|
||||||
|
</remote-repository>
|
||||||
|
<remote-repository>
|
||||||
|
<option name="id" value="jboss.community" />
|
||||||
|
<option name="name" value="JBoss Community repository" />
|
||||||
|
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
|
||||||
|
</remote-repository>
|
||||||
|
<remote-repository>
|
||||||
|
<option name="id" value="MavenRepo" />
|
||||||
|
<option name="name" value="MavenRepo" />
|
||||||
|
<option name="url" value="https://repo.maven.apache.org/maven2/" />
|
||||||
|
</remote-repository>
|
||||||
|
<remote-repository>
|
||||||
|
<option name="id" value="maven" />
|
||||||
|
<option name="name" value="maven" />
|
||||||
|
<option name="url" value="https://maven.aliucord.com/snapshots" />
|
||||||
|
</remote-repository>
|
||||||
|
<remote-repository>
|
||||||
|
<option name="id" value="Google" />
|
||||||
|
<option name="name" value="Google" />
|
||||||
|
<option name="url" value="https://dl.google.com/dl/android/maven2/" />
|
||||||
|
</remote-repository>
|
||||||
|
<remote-repository>
|
||||||
|
<option name="id" value="maven2" />
|
||||||
|
<option name="name" value="maven2" />
|
||||||
|
<option name="url" value="https://jitpack.io" />
|
||||||
|
</remote-repository>
|
||||||
|
</component>
|
||||||
|
</project>
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="KotlinJpsPluginSettings">
|
||||||
|
<option name="version" value="1.7.20" />
|
||||||
|
</component>
|
||||||
|
</project>
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||||
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" project-jdk-name="jbr-17" project-jdk-type="JavaSDK" />
|
||||||
|
</project>
|
|
@ -0,0 +1 @@
|
||||||
|
/build
|
|
@ -0,0 +1,165 @@
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("com.android.application")
|
||||||
|
kotlin("android")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "dev.beefers.vendetta.manager"
|
||||||
|
compileSdk = 33
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
applicationId = "dev.beefers.vendetta.manager"
|
||||||
|
minSdk = 24
|
||||||
|
targetSdk = 33
|
||||||
|
versionCode = 1000
|
||||||
|
versionName = "1.0.0"
|
||||||
|
|
||||||
|
buildConfigField("String", "GIT_BRANCH", "\"${getCurrentBranch()}\"")
|
||||||
|
buildConfigField("String", "GIT_COMMIT", "\"${getLatestCommit()}\"")
|
||||||
|
buildConfigField("boolean", "GIT_LOCAL_COMMITS", "${hasLocalCommits()}")
|
||||||
|
buildConfigField("boolean", "GIT_LOCAL_CHANGES", "${hasLocalChanges()}")
|
||||||
|
|
||||||
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
vectorDrawables {
|
||||||
|
useSupportLibrary = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
named("release") {
|
||||||
|
isMinifyEnabled = false
|
||||||
|
setProguardFiles(listOf(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_11
|
||||||
|
targetCompatibility = JavaVersion.VERSION_11
|
||||||
|
}
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = "11"
|
||||||
|
freeCompilerArgs += listOf(
|
||||||
|
"-Xcontext-receivers",
|
||||||
|
"-P",
|
||||||
|
"plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=${buildDir.resolve("report").absolutePath}",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
buildFeatures {
|
||||||
|
compose = true
|
||||||
|
buildConfig = true
|
||||||
|
}
|
||||||
|
composeOptions {
|
||||||
|
kotlinCompilerExtensionVersion = "1.3.2"
|
||||||
|
}
|
||||||
|
androidComponents {
|
||||||
|
onVariants(selector().withBuildType("release")) {
|
||||||
|
it.packaging.resources.excludes.apply {
|
||||||
|
// Debug metadata
|
||||||
|
add("/**/*.version")
|
||||||
|
add("/kotlin-tooling-metadata.json")
|
||||||
|
// Kotlin debugging (https://github.com/Kotlin/kotlinx.coroutines/issues/2274)
|
||||||
|
add("/DebugProbesKt.bin")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
packagingOptions {
|
||||||
|
resources {
|
||||||
|
// Reflection symbol list (https://stackoverflow.com/a/41073782/13964629)
|
||||||
|
excludes += "/**/*.kotlin_builtins"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
configurations {
|
||||||
|
all {
|
||||||
|
exclude(module = "listenablefuture")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation("androidx.core:core-ktx:1.9.0")
|
||||||
|
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.3.1")
|
||||||
|
|
||||||
|
implementation(platform("androidx.compose:compose-bom:2022.10.00"))
|
||||||
|
implementation("androidx.activity:activity-compose:1.5.1")
|
||||||
|
implementation("androidx.compose.ui:ui")
|
||||||
|
implementation("androidx.compose.ui:ui-graphics")
|
||||||
|
implementation("androidx.compose.ui:ui-tooling-preview")
|
||||||
|
implementation("androidx.compose.material3:material3")
|
||||||
|
implementation("androidx.compose.material:material-icons-extended")
|
||||||
|
|
||||||
|
val koinVersion = "3.2.0"
|
||||||
|
|
||||||
|
implementation("io.insert-koin:koin-core:$koinVersion")
|
||||||
|
implementation("io.insert-koin:koin-android:$koinVersion")
|
||||||
|
implementation("io.insert-koin:koin-androidx-compose:$koinVersion")
|
||||||
|
|
||||||
|
val accompanistVersion = "0.29.1-alpha"
|
||||||
|
|
||||||
|
implementation("com.google.accompanist:accompanist-systemuicontroller:$accompanistVersion")
|
||||||
|
implementation("com.google.accompanist:accompanist-pager:$accompanistVersion")
|
||||||
|
|
||||||
|
val voyagerVersion = "1.0.0-rc03"
|
||||||
|
|
||||||
|
implementation("cafe.adriel.voyager:voyager-navigator:$voyagerVersion")
|
||||||
|
implementation("cafe.adriel.voyager:voyager-tab-navigator:$voyagerVersion")
|
||||||
|
implementation("cafe.adriel.voyager:voyager-transitions:$voyagerVersion")
|
||||||
|
implementation("cafe.adriel.voyager:voyager-koin:$voyagerVersion")
|
||||||
|
|
||||||
|
val ktorVersion = "2.1.1"
|
||||||
|
|
||||||
|
implementation("io.ktor:ktor-client-core:$ktorVersion")
|
||||||
|
implementation("io.ktor:ktor-client-cio:$ktorVersion")
|
||||||
|
implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion")
|
||||||
|
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
|
||||||
|
implementation("io.ktor:ktor-client-logging:$ktorVersion")
|
||||||
|
|
||||||
|
implementation("io.github.diamondminer88:zip-android:2.1.0@aar")
|
||||||
|
// implementation("com.android.tools.build:apksig:7.4.0-beta04")
|
||||||
|
implementation(files("libs/lspatch.jar"))
|
||||||
|
|
||||||
|
testImplementation("junit:junit:4.13.2")
|
||||||
|
androidTestImplementation("androidx.test.ext:junit:1.1.5")
|
||||||
|
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
|
||||||
|
androidTestImplementation(platform("androidx.compose:compose-bom:2022.10.00"))
|
||||||
|
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
|
||||||
|
debugImplementation("androidx.compose.ui:ui-tooling")
|
||||||
|
debugImplementation("androidx.compose.ui:ui-test-manifest")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCurrentBranch(): String? =
|
||||||
|
exec("git", "symbolic-ref", "--short", "HEAD")
|
||||||
|
|
||||||
|
fun getLatestCommit(): String? =
|
||||||
|
exec("git", "rev-parse", "--short", "HEAD")
|
||||||
|
|
||||||
|
fun hasLocalCommits(): Boolean {
|
||||||
|
val branch = getCurrentBranch() ?: return false
|
||||||
|
return exec("git", "log", "origin/$branch..HEAD")?.isNotEmpty() ?: false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hasLocalChanges(): Boolean =
|
||||||
|
exec("git", "status", "-s")?.isNotEmpty() ?: false
|
||||||
|
|
||||||
|
fun exec(vararg command: String): String? {
|
||||||
|
return try {
|
||||||
|
val stdout = ByteArrayOutputStream()
|
||||||
|
val errout = ByteArrayOutputStream()
|
||||||
|
|
||||||
|
exec {
|
||||||
|
commandLine = command.toList()
|
||||||
|
standardOutput = stdout
|
||||||
|
errorOutput = errout
|
||||||
|
isIgnoreExitValue = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if(errout.size() > 0)
|
||||||
|
throw Error(errout.toString(Charsets.UTF_8))
|
||||||
|
|
||||||
|
stdout.toString(Charsets.UTF_8).trim()
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
e.printStackTrace()
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
Binary file not shown.
|
@ -0,0 +1,43 @@
|
||||||
|
## Keep `Companion` object fields of serializable classes.
|
||||||
|
## This avoids serializer lookup through `getDeclaredClasses` as done for named companion objects.
|
||||||
|
#-if @kotlinx.serialization.Serializable class **
|
||||||
|
#-keepclassmembers class <1> {
|
||||||
|
# static <1>$Companion Companion;
|
||||||
|
#}
|
||||||
|
#
|
||||||
|
## Keep `serializer()` on companion objects (both default and named) of serializable classes.
|
||||||
|
#-if @kotlinx.serialization.Serializable class ** {
|
||||||
|
# static **$* *;
|
||||||
|
#}
|
||||||
|
#-keepclassmembers class <2>$<3> {
|
||||||
|
# kotlinx.serialization.KSerializer serializer(...);
|
||||||
|
#}
|
||||||
|
#
|
||||||
|
## Keep `INSTANCE.serializer()` of serializable objects.
|
||||||
|
#-if @kotlinx.serialization.Serializable class ** {
|
||||||
|
# public static ** INSTANCE;
|
||||||
|
#}
|
||||||
|
#-keepclassmembers class <1> {
|
||||||
|
# public static <1> INSTANCE;
|
||||||
|
# kotlinx.serialization.KSerializer serializer(...);
|
||||||
|
#}
|
||||||
|
#
|
||||||
|
## @Serializable and @Polymorphic are used at runtime for polymorphic serialization.
|
||||||
|
#-keepattributes RuntimeVisibleAnnotations,AnnotationDefault
|
||||||
|
#
|
||||||
|
#-keepnames class <1>$$serializer { # -keepnames suffices; class is kept when serializer() is kept.
|
||||||
|
# static <1>$$serializer INSTANCE;
|
||||||
|
#}
|
||||||
|
|
||||||
|
# Uncomment this to preserve the line number information for
|
||||||
|
# debugging stack traces.
|
||||||
|
-keepattributes SourceFile,LineNumberTable
|
||||||
|
|
||||||
|
# Repackage classes into the top-level.
|
||||||
|
-repackageclasses
|
||||||
|
|
||||||
|
# Amount of optimization iterations, taken from an SO post
|
||||||
|
-optimizationpasses 5
|
||||||
|
|
||||||
|
# Broaden access modifiers to increase results during optimization
|
||||||
|
-allowaccessmodification
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"artifactType": {
|
||||||
|
"type": "APK",
|
||||||
|
"kind": "Directory"
|
||||||
|
},
|
||||||
|
"applicationId": "dev.beefers.vendetta.manager",
|
||||||
|
"variantName": "release",
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"type": "SINGLE",
|
||||||
|
"filters": [],
|
||||||
|
"attributes": [],
|
||||||
|
"versionCode": 1,
|
||||||
|
"versionName": "1.0.0",
|
||||||
|
"outputFile": "app-release.apk"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"elementType": "File"
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
package dev.beefers.vendetta.manager
|
||||||
|
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
import org.junit.Assert.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instrumented test, which will execute on an Android device.
|
||||||
|
*
|
||||||
|
* See [testing documentation](http://d.android.com/tools/testing).
|
||||||
|
*/
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class ExampleInstrumentedTest {
|
||||||
|
@Test
|
||||||
|
fun useAppContext() {
|
||||||
|
// Context of the app under test.
|
||||||
|
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
|
assertEquals("dev.beefers.vendetta.manager", appContext.packageName)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
|
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" />
|
||||||
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||||
|
android:maxSdkVersion="32" />
|
||||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||||
|
android:maxSdkVersion="32" />
|
||||||
|
<uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />
|
||||||
|
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
|
||||||
|
tools:ignore="ScopedStorage" />
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.REQUEST_DELETE_PACKAGES"
|
||||||
|
tools:ignore="ProtectedPermissions" />
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.DELETE_PACKAGES"
|
||||||
|
tools:ignore="ProtectedPermissions" />
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||||
|
tools:ignore="QueryAllPackagesPermission" />
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:allowBackup="true"
|
||||||
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
|
android:icon="@drawable/ic_launcher"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:roundIcon="@drawable/ic_launcher"
|
||||||
|
android:supportsRtl="true"
|
||||||
|
android:theme="@style/Theme.VendettaManager"
|
||||||
|
android:name=".ManagerApplication"
|
||||||
|
tools:targetApi="31">
|
||||||
|
<activity
|
||||||
|
android:name=".ui.activity.MainActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:theme="@style/Theme.VendettaManager">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
<service android:name=".installer.service.InstallService" />
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
Binary file not shown.
After Width: | Height: | Size: 7.7 KiB |
Binary file not shown.
After Width: | Height: | Size: 6.5 KiB |
Binary file not shown.
After Width: | Height: | Size: 3.0 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,26 @@
|
||||||
|
package dev.beefers.vendetta.manager
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import dev.beefers.vendetta.manager.di.httpModule
|
||||||
|
import dev.beefers.vendetta.manager.di.managerModule
|
||||||
|
import dev.beefers.vendetta.manager.di.repositoryModule
|
||||||
|
import dev.beefers.vendetta.manager.di.viewModelModule
|
||||||
|
import org.koin.android.ext.koin.androidContext
|
||||||
|
import org.koin.core.context.startKoin
|
||||||
|
|
||||||
|
class ManagerApplication : Application() {
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
startKoin {
|
||||||
|
androidContext(this@ManagerApplication)
|
||||||
|
modules(
|
||||||
|
httpModule,
|
||||||
|
managerModule,
|
||||||
|
viewModelModule,
|
||||||
|
repositoryModule
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
package dev.beefers.vendetta.manager.di
|
||||||
|
|
||||||
|
import dev.beefers.vendetta.manager.network.service.GithubService
|
||||||
|
import dev.beefers.vendetta.manager.network.service.HttpService
|
||||||
|
import io.ktor.client.HttpClient
|
||||||
|
import io.ktor.client.engine.cio.CIO
|
||||||
|
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||||
|
import io.ktor.serialization.kotlinx.json.json
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import org.koin.core.module.dsl.singleOf
|
||||||
|
import org.koin.dsl.module
|
||||||
|
|
||||||
|
val httpModule = module {
|
||||||
|
|
||||||
|
fun provideJson() = Json {
|
||||||
|
ignoreUnknownKeys = true
|
||||||
|
coerceInputValues = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun provideHttpClient(json: Json) = HttpClient(CIO) {
|
||||||
|
install(ContentNegotiation) {
|
||||||
|
json(json)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
singleOf(::provideJson)
|
||||||
|
singleOf(::provideHttpClient)
|
||||||
|
singleOf(::HttpService)
|
||||||
|
singleOf(::GithubService)
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
package dev.beefers.vendetta.manager.di
|
||||||
|
|
||||||
|
import dev.beefers.vendetta.manager.domain.manager.DownloadManager
|
||||||
|
import dev.beefers.vendetta.manager.domain.manager.PreferenceManager
|
||||||
|
import org.koin.core.module.dsl.singleOf
|
||||||
|
import org.koin.dsl.module
|
||||||
|
|
||||||
|
val managerModule = module {
|
||||||
|
singleOf(::DownloadManager)
|
||||||
|
singleOf(::PreferenceManager)
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
package dev.beefers.vendetta.manager.di
|
||||||
|
|
||||||
|
import dev.beefers.vendetta.manager.domain.repository.GithubRepository
|
||||||
|
import org.koin.core.module.dsl.singleOf
|
||||||
|
import org.koin.dsl.module
|
||||||
|
|
||||||
|
val repositoryModule = module {
|
||||||
|
singleOf(::GithubRepository)
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
package dev.beefers.vendetta.manager.di
|
||||||
|
|
||||||
|
import dev.beefers.vendetta.manager.ui.viewmodel.installer.InstallerViewModel
|
||||||
|
import dev.beefers.vendetta.manager.ui.viewmodel.main.MainViewModel
|
||||||
|
import dev.beefers.vendetta.manager.ui.viewmodel.settings.SettingsViewModel
|
||||||
|
import org.koin.core.module.dsl.factoryOf
|
||||||
|
import org.koin.dsl.module
|
||||||
|
|
||||||
|
val viewModelModule = module {
|
||||||
|
factoryOf(::InstallerViewModel)
|
||||||
|
factoryOf(::SettingsViewModel)
|
||||||
|
factoryOf(::MainViewModel)
|
||||||
|
}
|
|
@ -0,0 +1,134 @@
|
||||||
|
package dev.beefers.vendetta.manager.domain.manager
|
||||||
|
|
||||||
|
import android.app.DownloadManager
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import androidx.core.content.getSystemService
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import dev.beefers.vendetta.manager.BuildConfig
|
||||||
|
import java.io.File
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
import kotlin.coroutines.resumeWithException
|
||||||
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
|
||||||
|
class DownloadManager(
|
||||||
|
private val context: Context
|
||||||
|
) {
|
||||||
|
private val downloadManager = context.getSystemService<DownloadManager>()!!
|
||||||
|
|
||||||
|
// Discord APK downloading
|
||||||
|
suspend fun downloadDiscordApk(version: String, out: File): File =
|
||||||
|
download("$BACKEND_HOST/download/discord?v=$version", out)
|
||||||
|
|
||||||
|
suspend fun downloadSplit(version: String, split: String, out: File): File =
|
||||||
|
download("$BACKEND_HOST/download/discord?v=$version&split=$split", out)
|
||||||
|
|
||||||
|
suspend fun downloadVendetta(out: File) =
|
||||||
|
download(
|
||||||
|
"https://github.com/vendetta-mod/VendettaXposed/releases/latest/download/app-release.apk",
|
||||||
|
out
|
||||||
|
)
|
||||||
|
|
||||||
|
suspend fun downloadUpdate(out: File) =
|
||||||
|
download(
|
||||||
|
"https://github.com/vendetta-mod/VendettaManager/releases/latest/download/Manager.apk",
|
||||||
|
out
|
||||||
|
)
|
||||||
|
|
||||||
|
suspend fun download(url: String, out: File): File {
|
||||||
|
out.parentFile?.mkdirs()
|
||||||
|
|
||||||
|
return suspendCoroutine { continuation ->
|
||||||
|
val receiver = object : BroadcastReceiver() {
|
||||||
|
var downloadId = 0L
|
||||||
|
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
val id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1)
|
||||||
|
|
||||||
|
if (downloadId != id) return
|
||||||
|
|
||||||
|
val (status, reason) = DownloadManager.Query().run {
|
||||||
|
setFilterById(downloadId)
|
||||||
|
|
||||||
|
val cursor = downloadManager.query(this)
|
||||||
|
.apply { moveToFirst() }
|
||||||
|
|
||||||
|
// Notification's "cancel" was clicked
|
||||||
|
if (cursor.count == 0) {
|
||||||
|
-1 to -1
|
||||||
|
} else {
|
||||||
|
val status =
|
||||||
|
cursor.run { getInt(getColumnIndex(DownloadManager.COLUMN_STATUS)) }
|
||||||
|
val reason =
|
||||||
|
cursor.run { getInt(getColumnIndex(DownloadManager.COLUMN_REASON)) }
|
||||||
|
status to reason
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
when (status) {
|
||||||
|
// Cancelled
|
||||||
|
-1 -> {
|
||||||
|
context.unregisterReceiver(this)
|
||||||
|
continuation.resumeWithException(Error("Download was cancelled manually"))
|
||||||
|
}
|
||||||
|
|
||||||
|
DownloadManager.STATUS_SUCCESSFUL -> {
|
||||||
|
context.unregisterReceiver(this)
|
||||||
|
continuation.resume(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
DownloadManager.STATUS_FAILED -> {
|
||||||
|
context.unregisterReceiver(this)
|
||||||
|
continuation.resumeWithException(
|
||||||
|
Error(
|
||||||
|
"Failed to download $url because of: ${
|
||||||
|
reasonToString(
|
||||||
|
reason
|
||||||
|
)
|
||||||
|
}"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context.registerReceiver(
|
||||||
|
receiver,
|
||||||
|
IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
|
||||||
|
)
|
||||||
|
|
||||||
|
receiver.downloadId = DownloadManager.Request(url.toUri()).run {
|
||||||
|
setTitle("Vendetta Manager: Downloading ${out.name}")
|
||||||
|
setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE)
|
||||||
|
setDestinationUri(out.toUri())
|
||||||
|
addRequestHeader("User-Agent", "Vendetta Manager/${BuildConfig.VERSION_NAME}")
|
||||||
|
downloadManager.enqueue(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun reasonToString(int: Int): String {
|
||||||
|
return when (int) {
|
||||||
|
DownloadManager.ERROR_UNKNOWN -> "Unknown Error"
|
||||||
|
DownloadManager.ERROR_FILE_ERROR -> "File Error"
|
||||||
|
DownloadManager.ERROR_UNHANDLED_HTTP_CODE -> "Unhandled HTTP Code"
|
||||||
|
DownloadManager.ERROR_HTTP_DATA_ERROR -> "HTTP Data Error"
|
||||||
|
DownloadManager.ERROR_TOO_MANY_REDIRECTS -> "Too many redirects"
|
||||||
|
DownloadManager.ERROR_INSUFFICIENT_SPACE -> "Insufficient space"
|
||||||
|
DownloadManager.ERROR_DEVICE_NOT_FOUND -> "Device not found"
|
||||||
|
DownloadManager.ERROR_CANNOT_RESUME -> "Cannot resume download"
|
||||||
|
DownloadManager.ERROR_FILE_ALREADY_EXISTS -> "File already exists"
|
||||||
|
else -> "Unknown error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val BACKEND_HOST = "https://aliucord.com/"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
package dev.beefers.vendetta.manager.domain.manager
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import dev.beefers.vendetta.manager.R
|
||||||
|
import dev.beefers.vendetta.manager.domain.manager.base.BasePreferenceManager
|
||||||
|
|
||||||
|
class PreferenceManager(private val context: Context) :
|
||||||
|
BasePreferenceManager(context.getSharedPreferences("prefs", Context.MODE_PRIVATE)) {
|
||||||
|
|
||||||
|
var packageName by stringPreference("package_name", "dev.beefers.vendetta")
|
||||||
|
|
||||||
|
var appName by stringPreference("app_name", "Vendetta")
|
||||||
|
|
||||||
|
var discordVersion by stringPreference("discord_version", "")
|
||||||
|
|
||||||
|
var patchIcon by booleanPreference("patch_icon", true)
|
||||||
|
|
||||||
|
var debuggable by booleanPreference("debuggable", false)
|
||||||
|
|
||||||
|
var monet by booleanPreference("monet", Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
|
||||||
|
|
||||||
|
var isDeveloper by booleanPreference("is_developer", true)
|
||||||
|
|
||||||
|
var theme by enumPreference("theme", Theme.SYSTEM)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class Theme(@StringRes val labelRes: Int) {
|
||||||
|
SYSTEM(R.string.theme_system),
|
||||||
|
LIGHT(R.string.theme_light),
|
||||||
|
DARK(R.string.theme_dark)
|
||||||
|
}
|
|
@ -0,0 +1,115 @@
|
||||||
|
package dev.beefers.vendetta.manager.domain.manager.base
|
||||||
|
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.core.content.edit
|
||||||
|
import kotlin.reflect.KProperty
|
||||||
|
|
||||||
|
abstract class BasePreferenceManager(
|
||||||
|
private val prefs: SharedPreferences
|
||||||
|
) {
|
||||||
|
protected fun getString(key: String, defaultValue: String?) =
|
||||||
|
prefs.getString(key, defaultValue)!!
|
||||||
|
|
||||||
|
private fun getBoolean(key: String, defaultValue: Boolean) = prefs.getBoolean(key, defaultValue)
|
||||||
|
private fun getInt(key: String, defaultValue: Int) = prefs.getInt(key, defaultValue)
|
||||||
|
private fun getFloat(key: String, defaultValue: Float) = prefs.getFloat(key, defaultValue)
|
||||||
|
private fun getColor(key: String, defaultValue: Color): Color {
|
||||||
|
val c = prefs.getString(key, null)
|
||||||
|
return if (c == null) defaultValue else Color(c.toULong())
|
||||||
|
}
|
||||||
|
|
||||||
|
protected inline fun <reified E : Enum<E>> getEnum(key: String, defaultValue: E) =
|
||||||
|
enumValueOf<E>(getString(key, defaultValue.name))
|
||||||
|
|
||||||
|
protected fun putString(key: String, value: String?) = prefs.edit { putString(key, value) }
|
||||||
|
private fun putBoolean(key: String, value: Boolean) = prefs.edit { putBoolean(key, value) }
|
||||||
|
private fun putInt(key: String, value: Int) = prefs.edit { putInt(key, value) }
|
||||||
|
private fun putFloat(key: String, value: Float) = prefs.edit { putFloat(key, value) }
|
||||||
|
private fun putColor(key: String, value: Color) =
|
||||||
|
prefs.edit { putString(key, value.value.toString()) }
|
||||||
|
|
||||||
|
protected inline fun <reified E : Enum<E>> putEnum(key: String, value: E) =
|
||||||
|
putString(key, value.name)
|
||||||
|
|
||||||
|
protected class Preference<T>(
|
||||||
|
private val key: String,
|
||||||
|
defaultValue: T,
|
||||||
|
getter: (key: String, defaultValue: T) -> T,
|
||||||
|
private val setter: (key: String, newValue: T) -> Unit
|
||||||
|
) {
|
||||||
|
@Suppress("RedundantSetter")
|
||||||
|
var value by mutableStateOf(getter(key, defaultValue))
|
||||||
|
private set
|
||||||
|
|
||||||
|
operator fun getValue(thisRef: Any?, property: KProperty<*>) = value
|
||||||
|
operator fun setValue(thisRef: Any?, property: KProperty<*>, newValue: T) {
|
||||||
|
value = newValue
|
||||||
|
setter(key, newValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun stringPreference(
|
||||||
|
key: String,
|
||||||
|
defaultValue: String = ""
|
||||||
|
) = Preference(
|
||||||
|
key = key,
|
||||||
|
defaultValue = defaultValue,
|
||||||
|
getter = ::getString,
|
||||||
|
setter = ::putString
|
||||||
|
)
|
||||||
|
|
||||||
|
protected fun booleanPreference(
|
||||||
|
key: String,
|
||||||
|
defaultValue: Boolean
|
||||||
|
) = Preference(
|
||||||
|
key = key,
|
||||||
|
defaultValue = defaultValue,
|
||||||
|
getter = ::getBoolean,
|
||||||
|
setter = ::putBoolean
|
||||||
|
)
|
||||||
|
|
||||||
|
protected fun intPreference(
|
||||||
|
key: String,
|
||||||
|
defaultValue: Int
|
||||||
|
) = Preference(
|
||||||
|
key = key,
|
||||||
|
defaultValue = defaultValue,
|
||||||
|
getter = ::getInt,
|
||||||
|
setter = ::putInt
|
||||||
|
)
|
||||||
|
|
||||||
|
protected fun floatPreference(
|
||||||
|
key: String,
|
||||||
|
defaultValue: Float
|
||||||
|
) = Preference(
|
||||||
|
key = key,
|
||||||
|
defaultValue = defaultValue,
|
||||||
|
getter = ::getFloat,
|
||||||
|
setter = ::putFloat
|
||||||
|
)
|
||||||
|
|
||||||
|
protected fun colorPreference(
|
||||||
|
key: String,
|
||||||
|
defaultValue: Color
|
||||||
|
) = Preference(
|
||||||
|
key = key,
|
||||||
|
defaultValue = defaultValue,
|
||||||
|
getter = ::getColor,
|
||||||
|
setter = ::putColor
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
protected inline fun <reified E : Enum<E>> enumPreference(
|
||||||
|
key: String,
|
||||||
|
defaultValue: E
|
||||||
|
) = Preference(
|
||||||
|
key = key,
|
||||||
|
defaultValue = defaultValue,
|
||||||
|
getter = ::getEnum,
|
||||||
|
setter = ::putEnum
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
package dev.beefers.vendetta.manager.domain.repository
|
||||||
|
|
||||||
|
import dev.beefers.vendetta.manager.network.service.GithubService
|
||||||
|
|
||||||
|
class GithubRepository(
|
||||||
|
private val service: GithubService
|
||||||
|
) {
|
||||||
|
|
||||||
|
suspend fun getLatestRelease() = service.getLatestRelease()
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
package dev.beefers.vendetta.manager.installer.service
|
||||||
|
|
||||||
|
import android.app.Service
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageInstaller
|
||||||
|
import android.os.IBinder
|
||||||
|
import dev.beefers.vendetta.manager.R
|
||||||
|
import dev.beefers.vendetta.manager.utils.showToast
|
||||||
|
|
||||||
|
class InstallService : Service() {
|
||||||
|
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
|
||||||
|
when (val statusCode = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -999)) {
|
||||||
|
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
|
||||||
|
@Suppress("DEPRECATION") // No.
|
||||||
|
val confirmationIntent = intent.getParcelableExtra<Intent>(Intent.EXTRA_INTENT)!!
|
||||||
|
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
|
||||||
|
startActivity(confirmationIntent)
|
||||||
|
}
|
||||||
|
|
||||||
|
PackageInstaller.STATUS_SUCCESS -> showToast(R.string.installer_success)
|
||||||
|
PackageInstaller.STATUS_FAILURE_ABORTED -> showToast(R.string.installer_aborted)
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
showToast(R.string.installer_failed, statusCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stopSelf()
|
||||||
|
return START_NOT_STICKY
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBind(intent: Intent): IBinder? = null
|
||||||
|
}
|
|
@ -0,0 +1,236 @@
|
||||||
|
package dev.beefers.vendetta.manager.installer.util
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.os.Build
|
||||||
|
import pxb.android.axml.AxmlReader
|
||||||
|
import pxb.android.axml.AxmlVisitor
|
||||||
|
import pxb.android.axml.AxmlWriter
|
||||||
|
import pxb.android.axml.NodeVisitor
|
||||||
|
|
||||||
|
object ManifestPatcher {
|
||||||
|
private const val ANDROID_NAMESPACE = "http://schemas.android.com/apk/res/android"
|
||||||
|
private const val USES_CLEARTEXT_TRAFFIC = "usesCleartextTraffic"
|
||||||
|
private const val DEBUGGABLE = "debuggable"
|
||||||
|
private const val REQUEST_LEGACY_EXTERNAL_STORAGE = "requestLegacyExternalStorage"
|
||||||
|
private const val NETWORK_SECURITY_CONFIG = "networkSecurityConfig"
|
||||||
|
private const val LABEL = "label"
|
||||||
|
private const val PACKAGE = "package"
|
||||||
|
private const val COMPILE_SDK_VERSION = "compileSdkVersion"
|
||||||
|
private const val COMPILE_SDK_VERSION_CODENAME = "compileSdkVersionCodename"
|
||||||
|
|
||||||
|
fun patchManifest(
|
||||||
|
manifestBytes: ByteArray,
|
||||||
|
packageName: String,
|
||||||
|
appName: String,
|
||||||
|
debuggable: Boolean
|
||||||
|
): ByteArray {
|
||||||
|
val reader = AxmlReader(manifestBytes)
|
||||||
|
val writer = AxmlWriter()
|
||||||
|
|
||||||
|
reader.accept(object : AxmlVisitor(writer) {
|
||||||
|
override fun child(ns: String?, name: String?) =
|
||||||
|
object : ReplaceAttrsVisitor(
|
||||||
|
super.child(ns, name),
|
||||||
|
mapOf(
|
||||||
|
PACKAGE to packageName,
|
||||||
|
COMPILE_SDK_VERSION to 23,
|
||||||
|
COMPILE_SDK_VERSION_CODENAME to "6.0-2438415"
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
private var addExternalStoragePerm = false
|
||||||
|
|
||||||
|
override fun child(ns: String?, name: String): NodeVisitor {
|
||||||
|
val nv = super.child(ns, name)
|
||||||
|
|
||||||
|
// Add MANAGE_EXTERNAL_STORAGE when necessary
|
||||||
|
if (addExternalStoragePerm) {
|
||||||
|
super
|
||||||
|
.child(null, "uses-permission")
|
||||||
|
.attr(
|
||||||
|
ANDROID_NAMESPACE,
|
||||||
|
"name",
|
||||||
|
android.R.attr.name,
|
||||||
|
TYPE_STRING,
|
||||||
|
Manifest.permission.MANAGE_EXTERNAL_STORAGE
|
||||||
|
)
|
||||||
|
addExternalStoragePerm = false
|
||||||
|
}
|
||||||
|
|
||||||
|
return when (name) {
|
||||||
|
"uses-permission" -> object : NodeVisitor(nv) {
|
||||||
|
override fun attr(
|
||||||
|
ns: String?,
|
||||||
|
name: String?,
|
||||||
|
resourceId: Int,
|
||||||
|
type: Int,
|
||||||
|
value: Any?
|
||||||
|
) {
|
||||||
|
if (name != "maxSdkVersion") {
|
||||||
|
super.attr(ns, name, resourceId, type, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the add external storage permission to be added after WRITE_EXTERNAL_STORAGE (which is after read)
|
||||||
|
if (name == "name" && value == Manifest.permission.READ_EXTERNAL_STORAGE) {
|
||||||
|
addExternalStoragePerm = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"uses-sdk" -> object : NodeVisitor(nv) {
|
||||||
|
override fun attr(
|
||||||
|
ns: String?,
|
||||||
|
name: String?,
|
||||||
|
resourceId: Int,
|
||||||
|
type: Int,
|
||||||
|
value: Any?
|
||||||
|
) {
|
||||||
|
if (name == "targetSdkVersion") {
|
||||||
|
val version = if (Build.VERSION.SDK_INT >= 31) 30 else 28
|
||||||
|
super.attr(ns, name, resourceId, type, version)
|
||||||
|
} else {
|
||||||
|
super.attr(ns, name, resourceId, type, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"application" -> object : ReplaceAttrsVisitor(
|
||||||
|
nv,
|
||||||
|
mapOf(
|
||||||
|
LABEL to appName,
|
||||||
|
DEBUGGABLE to debuggable,
|
||||||
|
USES_CLEARTEXT_TRAFFIC to true,
|
||||||
|
REQUEST_LEGACY_EXTERNAL_STORAGE to true
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
private var addDebuggable = debuggable
|
||||||
|
private var addLegacyStorage = true
|
||||||
|
private var addUseClearTextTraffic = true
|
||||||
|
|
||||||
|
override fun attr(
|
||||||
|
ns: String?,
|
||||||
|
name: String,
|
||||||
|
resourceId: Int,
|
||||||
|
type: Int,
|
||||||
|
value: Any?
|
||||||
|
) {
|
||||||
|
if (name == NETWORK_SECURITY_CONFIG) return
|
||||||
|
super.attr(ns, name, resourceId, type, value)
|
||||||
|
if (name == REQUEST_LEGACY_EXTERNAL_STORAGE) addLegacyStorage =
|
||||||
|
false
|
||||||
|
if (name == DEBUGGABLE) addDebuggable = false
|
||||||
|
if (name == USES_CLEARTEXT_TRAFFIC) addUseClearTextTraffic =
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun child(ns: String?, name: String): NodeVisitor {
|
||||||
|
val visitor = super.child(ns, name)
|
||||||
|
|
||||||
|
return when (name) {
|
||||||
|
"activity" -> ReplaceAttrsVisitor(
|
||||||
|
visitor,
|
||||||
|
mapOf("label" to appName)
|
||||||
|
)
|
||||||
|
|
||||||
|
"provider" -> object : NodeVisitor(visitor) {
|
||||||
|
override fun attr(
|
||||||
|
ns: String?,
|
||||||
|
name: String,
|
||||||
|
resourceId: Int,
|
||||||
|
type: Int,
|
||||||
|
value: Any?
|
||||||
|
) {
|
||||||
|
super.attr(
|
||||||
|
ns,
|
||||||
|
name,
|
||||||
|
resourceId,
|
||||||
|
type,
|
||||||
|
if (name == "authorities") {
|
||||||
|
(value as String).replace(
|
||||||
|
"com.discord",
|
||||||
|
packageName
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
value
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> visitor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun end() {
|
||||||
|
if (addLegacyStorage) super.attr(
|
||||||
|
ANDROID_NAMESPACE,
|
||||||
|
REQUEST_LEGACY_EXTERNAL_STORAGE,
|
||||||
|
-1,
|
||||||
|
TYPE_INT_BOOLEAN,
|
||||||
|
1
|
||||||
|
)
|
||||||
|
if (addDebuggable) super.attr(
|
||||||
|
ANDROID_NAMESPACE,
|
||||||
|
DEBUGGABLE,
|
||||||
|
-1,
|
||||||
|
TYPE_INT_BOOLEAN,
|
||||||
|
1
|
||||||
|
)
|
||||||
|
if (addUseClearTextTraffic) super.attr(
|
||||||
|
ANDROID_NAMESPACE,
|
||||||
|
USES_CLEARTEXT_TRAFFIC,
|
||||||
|
-1,
|
||||||
|
TYPE_INT_BOOLEAN,
|
||||||
|
1
|
||||||
|
)
|
||||||
|
super.end()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> nv
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return writer.toByteArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun renamePackage(
|
||||||
|
manifestBytes: ByteArray,
|
||||||
|
packageName: String
|
||||||
|
): ByteArray {
|
||||||
|
val reader = AxmlReader(manifestBytes)
|
||||||
|
val writer = AxmlWriter()
|
||||||
|
|
||||||
|
reader.accept(
|
||||||
|
object : AxmlVisitor(writer) {
|
||||||
|
override fun child(ns: String?, name: String?): ReplaceAttrsVisitor {
|
||||||
|
return ReplaceAttrsVisitor(
|
||||||
|
super.child(ns, name),
|
||||||
|
mapOf("package" to packageName)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return writer.toByteArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
private open class ReplaceAttrsVisitor(
|
||||||
|
nv: NodeVisitor,
|
||||||
|
private val attrs: Map<String, Any>
|
||||||
|
) : NodeVisitor(nv) {
|
||||||
|
override fun attr(ns: String?, name: String, resourceId: Int, type: Int, value: Any?) {
|
||||||
|
val replace = attrs.containsKey(name)
|
||||||
|
val newValue = attrs[name]
|
||||||
|
|
||||||
|
super.attr(
|
||||||
|
ns,
|
||||||
|
name,
|
||||||
|
resourceId,
|
||||||
|
if (newValue is String) TYPE_STRING else type,
|
||||||
|
if (replace) newValue else value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
package dev.beefers.vendetta.manager.installer.util
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageInstaller.SessionParams
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Build
|
||||||
|
import dev.beefers.vendetta.manager.installer.service.InstallService
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
fun Context.installApks(silent: Boolean = false, vararg apks: File) {
|
||||||
|
val packageInstaller = packageManager.packageInstaller
|
||||||
|
val params = SessionParams(SessionParams.MODE_FULL_INSTALL).apply {
|
||||||
|
if (Build.VERSION.SDK_INT >= 31) {
|
||||||
|
setInstallScenario(PackageManager.INSTALL_SCENARIO_FAST)
|
||||||
|
|
||||||
|
if (silent) {
|
||||||
|
setRequireUserAction(SessionParams.USER_ACTION_NOT_REQUIRED)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val sessionId = packageInstaller.createSession(params)
|
||||||
|
val session = packageInstaller.openSession(sessionId)
|
||||||
|
|
||||||
|
apks.forEach { apk ->
|
||||||
|
session.openWrite(apk.name, 0, apk.length()).use {
|
||||||
|
it.write(apk.readBytes())
|
||||||
|
session.fsync(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val callbackIntent = Intent(this, InstallService::class.java)
|
||||||
|
|
||||||
|
@SuppressLint("UnspecifiedImmutableFlag")
|
||||||
|
val contentIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
PendingIntent.getService(this, 0, callbackIntent, PendingIntent.FLAG_MUTABLE)
|
||||||
|
} else {
|
||||||
|
PendingIntent.getService(this, 0, callbackIntent, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
session.commit(contentIntent.intentSender)
|
||||||
|
session.close()
|
||||||
|
}
|
|
@ -0,0 +1,64 @@
|
||||||
|
package dev.beefers.vendetta.manager.installer.util
|
||||||
|
|
||||||
|
import dev.beefers.vendetta.manager.network.utils.Signer
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.lsposed.lspatch.share.PatchConfig
|
||||||
|
import org.lsposed.patch.LSPatch
|
||||||
|
import org.lsposed.patch.util.Logger
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
object Patcher {
|
||||||
|
|
||||||
|
class Options(
|
||||||
|
private val config: PatchConfig,
|
||||||
|
private val outputDir: File,
|
||||||
|
private val apkPaths: List<String>,
|
||||||
|
private val embeddedModules: List<String>?
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun toStringArray(): Array<String> {
|
||||||
|
return buildList {
|
||||||
|
addAll(apkPaths)
|
||||||
|
add("-o"); add(outputDir.absolutePath)
|
||||||
|
if (config.debuggable) add("-d")
|
||||||
|
add("-l"); add(config.sigBypassLevel.toString())
|
||||||
|
if (config.useManager) add("--manager")
|
||||||
|
if (config.overrideVersionCode) add("-r")
|
||||||
|
add("-v")
|
||||||
|
embeddedModules?.forEach {
|
||||||
|
add("-m"); add(it)
|
||||||
|
}
|
||||||
|
addAll(arrayOf("-k", Signer.keyStore.absolutePath, "password", "alias", "password"))
|
||||||
|
}.toTypedArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun patch(
|
||||||
|
logger: Logger,
|
||||||
|
outputDir: File,
|
||||||
|
apkPaths: List<String>,
|
||||||
|
embeddedModules: List<String>
|
||||||
|
) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
LSPatch(
|
||||||
|
logger,
|
||||||
|
*apkPaths.toTypedArray(),
|
||||||
|
"-o",
|
||||||
|
outputDir.absolutePath,
|
||||||
|
"-l",
|
||||||
|
"2",
|
||||||
|
"-v",
|
||||||
|
"-m",
|
||||||
|
*embeddedModules.toTypedArray(),
|
||||||
|
"-k",
|
||||||
|
Signer.keyStore.absolutePath,
|
||||||
|
"password",
|
||||||
|
"alias",
|
||||||
|
"password"
|
||||||
|
).doCommandLine()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
package dev.beefers.vendetta.manager.network.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Release(
|
||||||
|
@SerialName("tag_name") val versionCode: Int,
|
||||||
|
@SerialName("name") val versionName: String
|
||||||
|
)
|
|
@ -0,0 +1,18 @@
|
||||||
|
package dev.beefers.vendetta.manager.network.service
|
||||||
|
|
||||||
|
import dev.beefers.vendetta.manager.network.dto.Release
|
||||||
|
import io.ktor.client.request.url
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
class GithubService(
|
||||||
|
private val httpService: HttpService
|
||||||
|
) {
|
||||||
|
|
||||||
|
suspend fun getLatestRelease() = withContext(Dispatchers.IO) {
|
||||||
|
httpService.request<Release> {
|
||||||
|
url("https://api.github.com/repos/vendetta-mod/VendettaManager/releases/latest")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
package dev.beefers.vendetta.manager.network.service
|
||||||
|
|
||||||
|
import dev.beefers.vendetta.manager.network.utils.ApiError
|
||||||
|
import dev.beefers.vendetta.manager.network.utils.ApiFailure
|
||||||
|
import dev.beefers.vendetta.manager.network.utils.ApiResponse
|
||||||
|
import io.ktor.client.HttpClient
|
||||||
|
import io.ktor.client.request.HttpRequestBuilder
|
||||||
|
import io.ktor.client.request.request
|
||||||
|
import io.ktor.client.statement.bodyAsText
|
||||||
|
import io.ktor.http.isSuccess
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
|
class HttpService(
|
||||||
|
val json: Json,
|
||||||
|
val http: HttpClient
|
||||||
|
) {
|
||||||
|
|
||||||
|
suspend inline fun <reified T> request(builder: HttpRequestBuilder.() -> Unit = {}): ApiResponse<T> {
|
||||||
|
var body: String? = null
|
||||||
|
|
||||||
|
val response = try {
|
||||||
|
val response = http.request(builder)
|
||||||
|
|
||||||
|
if (response.status.isSuccess()) {
|
||||||
|
body = response.bodyAsText()
|
||||||
|
|
||||||
|
if (T::class == String::class) {
|
||||||
|
return ApiResponse.Success(body as T)
|
||||||
|
}
|
||||||
|
|
||||||
|
ApiResponse.Success(json.decodeFromString<T>(body))
|
||||||
|
} else {
|
||||||
|
body = try {
|
||||||
|
response.bodyAsText()
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
ApiResponse.Error(ApiError(response.status, body))
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
ApiResponse.Failure(ApiFailure(e, body))
|
||||||
|
}
|
||||||
|
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
package dev.beefers.vendetta.manager.network.utils
|
||||||
|
|
||||||
|
import io.ktor.http.HttpStatusCode
|
||||||
|
|
||||||
|
sealed interface ApiResponse<D> {
|
||||||
|
data class Success<D>(val data: D) : ApiResponse<D>
|
||||||
|
data class Error<D>(val error: ApiError) : ApiResponse<D>
|
||||||
|
data class Failure<D>(val error: ApiFailure) : ApiResponse<D>
|
||||||
|
}
|
||||||
|
|
||||||
|
class ApiError(code: HttpStatusCode, body: String?) : Error("HTTP Code $code, Body: $body")
|
||||||
|
class ApiFailure(error: Throwable, body: String?) : Error(body, error)
|
||||||
|
|
||||||
|
val <D> ApiResponse<D>.dataOrNull
|
||||||
|
get() = if (this is ApiResponse.Success) data else null
|
||||||
|
|
||||||
|
val <D> ApiResponse<D>.dataOrThrow
|
||||||
|
get() = when (this) {
|
||||||
|
is ApiResponse.Success -> data
|
||||||
|
is ApiResponse.Error -> throw error
|
||||||
|
is ApiResponse.Failure -> throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <D> ApiResponse<D>.ifSuccessful(block: (D) -> Unit) {
|
||||||
|
if (this is ApiResponse.Success) block(data)
|
||||||
|
}
|
|
@ -0,0 +1,122 @@
|
||||||
|
package dev.beefers.vendetta.manager.network.utils
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
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.operator.jcajce.JcaContentSignerBuilder
|
||||||
|
import org.koin.core.component.KoinComponent
|
||||||
|
import org.koin.core.component.inject
|
||||||
|
import java.io.File
|
||||||
|
import java.math.BigInteger
|
||||||
|
import java.security.KeyPairGenerator
|
||||||
|
import java.security.KeyStore
|
||||||
|
import java.security.PrivateKey
|
||||||
|
import java.security.SecureRandom
|
||||||
|
import java.security.cert.Certificate
|
||||||
|
import java.security.cert.X509Certificate
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
object Signer : KoinComponent {
|
||||||
|
private val password = "password".toCharArray()
|
||||||
|
private val cacheDir = inject<Context>().value.cacheDir
|
||||||
|
|
||||||
|
val keyStore: File
|
||||||
|
get() {
|
||||||
|
lateinit var ks: File
|
||||||
|
cacheDir.resolve("ks.keystore").also {
|
||||||
|
if (!it.exists()) {
|
||||||
|
cacheDir.mkdir()
|
||||||
|
newKeystore(it)
|
||||||
|
}
|
||||||
|
ks = it
|
||||||
|
}
|
||||||
|
return ks
|
||||||
|
}
|
||||||
|
|
||||||
|
private val signerConfig: ApkSigner.SignerConfig by lazy {
|
||||||
|
val keyStore = KeyStore.getInstance(KeyStore.getDefaultType())
|
||||||
|
|
||||||
|
cacheDir.resolve("ks.keystore").also {
|
||||||
|
if (!it.exists()) {
|
||||||
|
cacheDir.mkdir()
|
||||||
|
newKeystore(it)
|
||||||
|
}
|
||||||
|
}.inputStream().use { stream ->
|
||||||
|
keyStore.load(stream, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
val alias = keyStore.aliases().nextElement()
|
||||||
|
val certificate = keyStore.getCertificate(alias) as X509Certificate
|
||||||
|
|
||||||
|
ApkSigner.SignerConfig.Builder(
|
||||||
|
"Vendetta",
|
||||||
|
keyStore.getKey(alias, password) as PrivateKey,
|
||||||
|
listOf(certificate)
|
||||||
|
).build()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun newKeystore(out: File?) {
|
||||||
|
val key = createKey()
|
||||||
|
|
||||||
|
with(KeyStore.getInstance(KeyStore.getDefaultType())) {
|
||||||
|
load(null, password)
|
||||||
|
setKeyEntry("alias", key.privateKey, password, arrayOf<Certificate>(key.publicKey))
|
||||||
|
store(out?.outputStream(), password)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createKey(): KeySet {
|
||||||
|
var serialNumber: BigInteger
|
||||||
|
|
||||||
|
do serialNumber = SecureRandom().nextInt().toBigInteger()
|
||||||
|
while (serialNumber < BigInteger.ZERO)
|
||||||
|
|
||||||
|
val x500Name = X500Name("CN=Vendetta Manager")
|
||||||
|
val pair = KeyPairGenerator.getInstance("RSA").run {
|
||||||
|
initialize(2048)
|
||||||
|
generateKeyPair()
|
||||||
|
}
|
||||||
|
val builder = X509v3CertificateBuilder(
|
||||||
|
/* issuer = */ x500Name,
|
||||||
|
/* serial = */
|
||||||
|
serialNumber,
|
||||||
|
/* notBefore = */
|
||||||
|
Date(System.currentTimeMillis() - 1000L * 60L * 60L * 24L * 30L),
|
||||||
|
/* notAfter = */
|
||||||
|
Date(System.currentTimeMillis() + 1000L * 60L * 60L * 24L * 366L * 30L),
|
||||||
|
/* dateLocale = */
|
||||||
|
Locale.ENGLISH,
|
||||||
|
/* subject = */
|
||||||
|
x500Name,
|
||||||
|
/* publicKeyInfo = */
|
||||||
|
SubjectPublicKeyInfo.getInstance(pair.public.encoded)
|
||||||
|
)
|
||||||
|
val signer = JcaContentSignerBuilder("SHA1withRSA").build(pair.private)
|
||||||
|
|
||||||
|
return KeySet(
|
||||||
|
JcaX509CertificateConverter().getCertificate(builder.build(signer)),
|
||||||
|
pair.private
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun signApk(apkFile: File, output: File) {
|
||||||
|
val outputApk = cacheDir.resolve(apkFile.name)
|
||||||
|
|
||||||
|
ApkSigner.Builder(listOf(signerConfig))
|
||||||
|
.setV1SigningEnabled(false) // TODO: enable so api <24 devices can work, however zip-alignment breaks
|
||||||
|
.setV2SigningEnabled(true)
|
||||||
|
.setV3SigningEnabled(true)
|
||||||
|
.setInputApk(apkFile)
|
||||||
|
.setOutputApk(output)
|
||||||
|
.build()
|
||||||
|
.sign()
|
||||||
|
|
||||||
|
outputApk.renameTo(apkFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
private class KeySet(val publicKey: X509Certificate, val privateKey: PrivateKey)
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
package dev.beefers.vendetta.manager.ui.activity
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
|
import cafe.adriel.voyager.navigator.Navigator
|
||||||
|
import cafe.adriel.voyager.transitions.SlideTransition
|
||||||
|
import dev.beefers.vendetta.manager.ui.screen.main.MainScreen
|
||||||
|
import dev.beefers.vendetta.manager.ui.theme.VendettaManagerTheme
|
||||||
|
|
||||||
|
class MainActivity : ComponentActivity() {
|
||||||
|
@OptIn(ExperimentalAnimationApi::class)
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContent {
|
||||||
|
VendettaManagerTheme {
|
||||||
|
Navigator(MainScreen()) {
|
||||||
|
SlideTransition(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
package dev.beefers.vendetta.manager.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.material3.RadioButton
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
inline fun <reified E : Enum<E>> EnumRadioController(
|
||||||
|
default: E,
|
||||||
|
labelFactory: (E) -> String = { it.toString() },
|
||||||
|
crossinline onChoiceSelected: (E) -> Unit
|
||||||
|
) {
|
||||||
|
var choice by remember { mutableStateOf(default) }
|
||||||
|
val ctx = LocalContext.current
|
||||||
|
|
||||||
|
Column {
|
||||||
|
enumValues<E>().forEach {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable {
|
||||||
|
choice = it
|
||||||
|
onChoiceSelected(it)
|
||||||
|
},
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(labelFactory(it))
|
||||||
|
Spacer(Modifier.weight(1f))
|
||||||
|
RadioButton(
|
||||||
|
selected = it == choice,
|
||||||
|
onClick = {
|
||||||
|
choice = it
|
||||||
|
onChoiceSelected(it)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
package dev.beefers.vendetta.manager.ui.components.settings
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.heightIn
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SettingsButton(
|
||||||
|
label: String,
|
||||||
|
onClick: () -> Unit = {}
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.heightIn(min = 64.dp)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 18.dp, vertical = 14.dp)
|
||||||
|
) {
|
||||||
|
Button(onClick, modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Text(text = label)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,54 @@
|
||||||
|
package dev.beefers.vendetta.manager.ui.components.settings
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.slideInVertically
|
||||||
|
import androidx.compose.animation.slideOutVertically
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.FilledTonalButton
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import dev.beefers.vendetta.manager.R
|
||||||
|
import dev.beefers.vendetta.manager.ui.components.EnumRadioController
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
inline fun <reified E : Enum<E>> SettingsChoiceDialog(
|
||||||
|
visible: Boolean = false,
|
||||||
|
default: E,
|
||||||
|
noinline title: @Composable () -> Unit,
|
||||||
|
crossinline labelFactory: (E) -> String = { it.toString() },
|
||||||
|
noinline onRequestClose: () -> Unit = {},
|
||||||
|
crossinline description: @Composable () -> Unit = {},
|
||||||
|
noinline onChoice: (E) -> Unit = {},
|
||||||
|
) {
|
||||||
|
|
||||||
|
var choice by remember { mutableStateOf(default) }
|
||||||
|
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = visible,
|
||||||
|
enter = slideInVertically(),
|
||||||
|
exit = slideOutVertically()
|
||||||
|
) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { onRequestClose() },
|
||||||
|
title = title,
|
||||||
|
text = {
|
||||||
|
description()
|
||||||
|
EnumRadioController(
|
||||||
|
default,
|
||||||
|
labelFactory
|
||||||
|
) { choice = it }
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
FilledTonalButton(onClick = { onChoice(choice) }) {
|
||||||
|
Text(text = stringResource(id = R.string.action_confirm))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
package dev.beefers.vendetta.manager.ui.components.settings
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SettingsHeader(
|
||||||
|
text: String
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
modifier = Modifier.padding(18.dp, 24.dp, 18.dp, 10.dp)
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,64 @@
|
||||||
|
package dev.beefers.vendetta.manager.ui.components.settings
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.heightIn
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.ProvideTextStyle
|
||||||
|
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
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SettingsItem(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
icon: @Composable (() -> Unit)? = null,
|
||||||
|
text: @Composable () -> Unit,
|
||||||
|
secondaryText: @Composable (() -> Unit) = { },
|
||||||
|
trailing: @Composable (() -> Unit) = { },
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.heightIn(min = 64.dp)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 18.dp, vertical = 14.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(14.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
if (icon != null) Box(modifier = Modifier.padding(8.dp)) {
|
||||||
|
icon()
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(5.dp)
|
||||||
|
) {
|
||||||
|
ProvideTextStyle(
|
||||||
|
MaterialTheme.typography.titleLarge.copy(
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
fontSize = 19.sp
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
text()
|
||||||
|
}
|
||||||
|
ProvideTextStyle(
|
||||||
|
MaterialTheme.typography.bodyMedium.copy(
|
||||||
|
color = MaterialTheme.colorScheme.onSurface.copy(0.6f)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
secondaryText()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.weight(1f, true))
|
||||||
|
|
||||||
|
trailing()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
package dev.beefers.vendetta.manager.ui.components.settings
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.material3.FilledTonalButton
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
inline fun <reified E : Enum<E>> SettingsItemChoice(
|
||||||
|
label: String,
|
||||||
|
title: String = label,
|
||||||
|
disabled: Boolean = false,
|
||||||
|
pref: E,
|
||||||
|
crossinline labelFactory: (E) -> String = { it.toString() },
|
||||||
|
crossinline onPrefChange: (E) -> Unit,
|
||||||
|
) {
|
||||||
|
val ctx = LocalContext.current
|
||||||
|
val choiceLabel = labelFactory(pref)
|
||||||
|
var opened = remember {
|
||||||
|
mutableStateOf(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsItem(
|
||||||
|
modifier = Modifier.clickable { opened.value = true },
|
||||||
|
text = { Text(text = label) },
|
||||||
|
) {
|
||||||
|
SettingsChoiceDialog(
|
||||||
|
visible = opened.value,
|
||||||
|
title = { Text(title) },
|
||||||
|
default = pref,
|
||||||
|
labelFactory = labelFactory,
|
||||||
|
onRequestClose = {
|
||||||
|
opened.value = false
|
||||||
|
},
|
||||||
|
onChoice = {
|
||||||
|
opened.value = false
|
||||||
|
onPrefChange(it)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
FilledTonalButton(onClick = { opened.value = true }, enabled = !disabled) {
|
||||||
|
Text(choiceLabel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
package dev.beefers.vendetta.manager.ui.components.settings
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.material3.Switch
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SettingsSwitch(
|
||||||
|
label: String,
|
||||||
|
secondaryLabel: String? = null,
|
||||||
|
disabled: Boolean = false,
|
||||||
|
pref: Boolean,
|
||||||
|
onPrefChange: (Boolean) -> Unit,
|
||||||
|
) {
|
||||||
|
SettingsItem(
|
||||||
|
modifier = Modifier.clickable(enabled = !disabled) { onPrefChange(!pref) },
|
||||||
|
text = { Text(text = label) },
|
||||||
|
secondaryText = {
|
||||||
|
secondaryLabel?.let {
|
||||||
|
Text(text = it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Switch(
|
||||||
|
checked = pref,
|
||||||
|
enabled = !disabled,
|
||||||
|
onCheckedChange = { onPrefChange(!pref) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
package dev.beefers.vendetta.manager.ui.components.settings
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun SettingsTextField(
|
||||||
|
label: String,
|
||||||
|
disabled: Boolean = false,
|
||||||
|
pref: String,
|
||||||
|
error: Boolean = false,
|
||||||
|
onPrefChange: (String) -> Unit,
|
||||||
|
) {
|
||||||
|
Box(modifier = Modifier.padding(horizontal = 18.dp, vertical = 10.dp)) {
|
||||||
|
OutlinedTextField(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
value = pref,
|
||||||
|
onValueChange = onPrefChange,
|
||||||
|
enabled = !disabled,
|
||||||
|
label = { Text(label) },
|
||||||
|
isError = error,
|
||||||
|
singleLine = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,64 @@
|
||||||
|
package dev.beefers.vendetta.manager.ui.screen.home
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Home
|
||||||
|
import androidx.compose.material.icons.outlined.Home
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||||
|
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||||
|
import cafe.adriel.voyager.navigator.tab.TabOptions
|
||||||
|
import dev.beefers.vendetta.manager.R
|
||||||
|
import dev.beefers.vendetta.manager.ui.screen.installer.InstallerScreen
|
||||||
|
import dev.beefers.vendetta.manager.utils.ManagerTab
|
||||||
|
import dev.beefers.vendetta.manager.utils.TabOptions
|
||||||
|
import dev.beefers.vendetta.manager.utils.navigate
|
||||||
|
|
||||||
|
class HomeScreen : ManagerTab {
|
||||||
|
override val options: TabOptions
|
||||||
|
@Composable get() = TabOptions(
|
||||||
|
title = R.string.title_home,
|
||||||
|
selectedIcon = Icons.Filled.Home,
|
||||||
|
unselectedIcon = Icons.Outlined.Home
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun Content() {
|
||||||
|
val nav = LocalNavigator.currentOrThrow
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(16.dp)
|
||||||
|
.fillMaxSize()
|
||||||
|
) {
|
||||||
|
Button(
|
||||||
|
onClick = { nav.navigate(InstallerScreen()) },
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.action_install))
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
Text(
|
||||||
|
text = "This UI is temporary, check back later for something prettier",
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun Actions() {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,110 @@
|
||||||
|
package dev.beefers.vendetta.manager.ui.screen.installer
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.ArrowBack
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import cafe.adriel.voyager.core.screen.Screen
|
||||||
|
import cafe.adriel.voyager.koin.getScreenModel
|
||||||
|
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||||
|
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||||
|
import dev.beefers.vendetta.manager.R
|
||||||
|
import dev.beefers.vendetta.manager.ui.viewmodel.installer.InstallerViewModel
|
||||||
|
import dev.beefers.vendetta.manager.ui.widgets.installer.StepGroupCard
|
||||||
|
|
||||||
|
class InstallerScreen : Screen {
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
override fun Content() {
|
||||||
|
val viewModel: InstallerViewModel = getScreenModel()
|
||||||
|
|
||||||
|
var expandedGroup by remember {
|
||||||
|
mutableStateOf<InstallerViewModel.InstallStepGroup?>(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(viewModel.currentStep) {
|
||||||
|
expandedGroup = viewModel.currentStep?.group
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = { TitleBar() }
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(it)
|
||||||
|
.padding(16.dp)
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
) {
|
||||||
|
for (group in InstallerViewModel.InstallStepGroup.values()) {
|
||||||
|
StepGroupCard(
|
||||||
|
name = stringResource(group.nameRes),
|
||||||
|
isCurrent = expandedGroup == group,
|
||||||
|
onClick = { expandedGroup = group },
|
||||||
|
steps = viewModel.getSteps(group),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (viewModel.isFinished) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp)
|
||||||
|
) {
|
||||||
|
Button(
|
||||||
|
onClick = { viewModel.copyDebugInfo() },
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.action_copy_logs))
|
||||||
|
}
|
||||||
|
Button(
|
||||||
|
onClick = { viewModel.clearCache() },
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.action_clear_cache))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
private fun TitleBar() {
|
||||||
|
val nav = LocalNavigator.currentOrThrow
|
||||||
|
TopAppBar(
|
||||||
|
title = { Text(stringResource(R.string.title_installer)) },
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = { nav.pop() }) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.ArrowBack,
|
||||||
|
contentDescription = stringResource(R.string.action_back)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,115 @@
|
||||||
|
package dev.beefers.vendetta.manager.ui.screen.main
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.NavigationBar
|
||||||
|
import androidx.compose.material3.NavigationBarItem
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.compositionLocalOf
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import cafe.adriel.voyager.core.screen.Screen
|
||||||
|
import cafe.adriel.voyager.koin.getScreenModel
|
||||||
|
import com.google.accompanist.pager.ExperimentalPagerApi
|
||||||
|
import com.google.accompanist.pager.HorizontalPager
|
||||||
|
import com.google.accompanist.pager.PagerState
|
||||||
|
import com.google.accompanist.pager.rememberPagerState
|
||||||
|
import dev.beefers.vendetta.manager.BuildConfig
|
||||||
|
import dev.beefers.vendetta.manager.ui.viewmodel.main.MainViewModel
|
||||||
|
import dev.beefers.vendetta.manager.ui.widgets.updater.UpdateDialog
|
||||||
|
import dev.beefers.vendetta.manager.utils.MainTab
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class MainScreen : Screen {
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalPagerApi::class)
|
||||||
|
@Composable
|
||||||
|
override fun Content() {
|
||||||
|
val viewModel: MainViewModel = getScreenModel()
|
||||||
|
val pagerState = rememberPagerState()
|
||||||
|
|
||||||
|
CompositionLocalProvider(
|
||||||
|
LocalPagerState provides pagerState
|
||||||
|
) {
|
||||||
|
Scaffold(
|
||||||
|
bottomBar = { NavBar() },
|
||||||
|
topBar = { TitleBar() },
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
) { pv ->
|
||||||
|
viewModel.release?.let {
|
||||||
|
if (it.versionCode > BuildConfig.VERSION_CODE) {
|
||||||
|
UpdateDialog(release = it) {
|
||||||
|
viewModel.downloadAndInstallUpdate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontalPager(
|
||||||
|
count = MainTab.values().size,
|
||||||
|
state = pagerState,
|
||||||
|
contentPadding = pv
|
||||||
|
) { page ->
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
) {
|
||||||
|
MainTab.values()[page].tab.Content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalPagerApi::class, ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun TitleBar() {
|
||||||
|
val pagerState = LocalPagerState.current
|
||||||
|
val tab = MainTab.values()[pagerState.currentPage].tab
|
||||||
|
|
||||||
|
TopAppBar(
|
||||||
|
title = { Text(tab.options.title) },
|
||||||
|
actions = { tab.Actions() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@OptIn(ExperimentalPagerApi::class)
|
||||||
|
private fun NavBar() {
|
||||||
|
val pagerState = LocalPagerState.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val tab = MainTab.values()[pagerState.currentPage].tab
|
||||||
|
|
||||||
|
NavigationBar {
|
||||||
|
MainTab.values().forEach { mainTab ->
|
||||||
|
NavigationBarItem(
|
||||||
|
selected = mainTab.tab === tab,
|
||||||
|
onClick = {
|
||||||
|
val page = MainTab.values().indexOf(mainTab)
|
||||||
|
scope.launch {
|
||||||
|
pagerState.animateScrollToPage(page)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
label = { Text(mainTab.tab.options.title) },
|
||||||
|
alwaysShowLabel = true,
|
||||||
|
icon = {
|
||||||
|
Icon(
|
||||||
|
painter = mainTab.tab.options.icon!!,
|
||||||
|
contentDescription = mainTab.tab.options.title
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalPagerApi::class)
|
||||||
|
val LocalPagerState = compositionLocalOf<PagerState> {
|
||||||
|
error("Pager not initialized")
|
||||||
|
}
|
|
@ -0,0 +1,95 @@
|
||||||
|
package dev.beefers.vendetta.manager.ui.screen.settings
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Settings
|
||||||
|
import androidx.compose.material.icons.outlined.Settings
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import cafe.adriel.voyager.koin.getScreenModel
|
||||||
|
import cafe.adriel.voyager.navigator.tab.TabOptions
|
||||||
|
import dev.beefers.vendetta.manager.R
|
||||||
|
import dev.beefers.vendetta.manager.domain.manager.PreferenceManager
|
||||||
|
import dev.beefers.vendetta.manager.ui.components.settings.SettingsButton
|
||||||
|
import dev.beefers.vendetta.manager.ui.components.settings.SettingsHeader
|
||||||
|
import dev.beefers.vendetta.manager.ui.components.settings.SettingsItemChoice
|
||||||
|
import dev.beefers.vendetta.manager.ui.components.settings.SettingsSwitch
|
||||||
|
import dev.beefers.vendetta.manager.ui.components.settings.SettingsTextField
|
||||||
|
import dev.beefers.vendetta.manager.ui.viewmodel.settings.SettingsViewModel
|
||||||
|
import dev.beefers.vendetta.manager.utils.ManagerTab
|
||||||
|
import dev.beefers.vendetta.manager.utils.TabOptions
|
||||||
|
import org.koin.androidx.compose.get
|
||||||
|
|
||||||
|
class SettingsScreen : ManagerTab {
|
||||||
|
override val options: TabOptions
|
||||||
|
@Composable get() = TabOptions(
|
||||||
|
title = R.string.title_settings,
|
||||||
|
selectedIcon = Icons.Filled.Settings,
|
||||||
|
unselectedIcon = Icons.Outlined.Settings
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun Content() {
|
||||||
|
val viewModel: SettingsViewModel = getScreenModel()
|
||||||
|
val prefs: PreferenceManager = get()
|
||||||
|
val ctx = LocalContext.current
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.verticalScroll(rememberScrollState())
|
||||||
|
) {
|
||||||
|
SettingsHeader(stringResource(R.string.settings_appearance))
|
||||||
|
SettingsSwitch(
|
||||||
|
label = stringResource(R.string.settings_dynamic_color),
|
||||||
|
secondaryLabel = stringResource(R.string.settings_dynamic_color_description),
|
||||||
|
pref = prefs.monet,
|
||||||
|
onPrefChange = {
|
||||||
|
prefs.monet = it
|
||||||
|
},
|
||||||
|
disabled = Build.VERSION.SDK_INT < Build.VERSION_CODES.S
|
||||||
|
)
|
||||||
|
SettingsItemChoice(
|
||||||
|
label = stringResource(R.string.settings_theme),
|
||||||
|
pref = prefs.theme,
|
||||||
|
labelFactory = {
|
||||||
|
ctx.getString(it.labelRes)
|
||||||
|
},
|
||||||
|
onPrefChange = {
|
||||||
|
prefs.theme = it
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
SettingsHeader(stringResource(R.string.settings_advanced))
|
||||||
|
SettingsTextField(
|
||||||
|
label = stringResource(R.string.settings_app_name),
|
||||||
|
pref = prefs.appName,
|
||||||
|
onPrefChange = {
|
||||||
|
prefs.appName = it
|
||||||
|
}
|
||||||
|
)
|
||||||
|
SettingsSwitch(
|
||||||
|
label = stringResource(R.string.settings_app_icon),
|
||||||
|
secondaryLabel = stringResource(R.string.settings_app_icon_description),
|
||||||
|
pref = prefs.patchIcon,
|
||||||
|
onPrefChange = {
|
||||||
|
prefs.patchIcon = it
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
SettingsButton(
|
||||||
|
label = stringResource(R.string.action_clear_cache),
|
||||||
|
onClick = {
|
||||||
|
viewModel.clearCache()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun Actions() {
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
package dev.beefers.vendetta.manager.ui.theme
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.darkColorScheme
|
||||||
|
import androidx.compose.material3.dynamicDarkColorScheme
|
||||||
|
import androidx.compose.material3.dynamicLightColorScheme
|
||||||
|
import androidx.compose.material3.lightColorScheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.SideEffect
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalView
|
||||||
|
import androidx.core.view.WindowCompat
|
||||||
|
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||||
|
import dev.beefers.vendetta.manager.domain.manager.PreferenceManager
|
||||||
|
import dev.beefers.vendetta.manager.domain.manager.Theme
|
||||||
|
import org.koin.androidx.compose.get
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun VendettaManagerTheme(
|
||||||
|
content: @Composable () -> Unit
|
||||||
|
) {
|
||||||
|
val prefs = get<PreferenceManager>()
|
||||||
|
val dynamicColor = prefs.monet
|
||||||
|
val darkTheme = when (prefs.theme) {
|
||||||
|
Theme.SYSTEM -> isSystemInDarkTheme()
|
||||||
|
Theme.DARK -> true
|
||||||
|
Theme.LIGHT -> false
|
||||||
|
}
|
||||||
|
val systemUiController = rememberSystemUiController()
|
||||||
|
val colorScheme = when {
|
||||||
|
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||||
|
val context = LocalContext.current
|
||||||
|
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
darkTheme -> darkColorScheme()
|
||||||
|
else -> lightColorScheme()
|
||||||
|
}
|
||||||
|
val view = LocalView.current
|
||||||
|
if (!view.isInEditMode) {
|
||||||
|
SideEffect {
|
||||||
|
val window = (view.context as Activity).window
|
||||||
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
|
systemUiController.setSystemBarsColor(Color.Transparent, darkIcons = !darkTheme)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MaterialTheme(
|
||||||
|
colorScheme = colorScheme,
|
||||||
|
typography = Typography,
|
||||||
|
content = content
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
package dev.beefers.vendetta.manager.ui.theme
|
||||||
|
|
||||||
|
import androidx.compose.material3.Typography
|
||||||
|
|
||||||
|
// Set of Material typography styles to start with
|
||||||
|
val Typography = Typography()
|
|
@ -0,0 +1,450 @@
|
||||||
|
package dev.beefers.vendetta.manager.ui.viewmodel.installer
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.runtime.Stable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateMapOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import cafe.adriel.voyager.core.model.ScreenModel
|
||||||
|
import cafe.adriel.voyager.core.model.coroutineScope
|
||||||
|
import com.github.diamondminer88.zip.ZipCompression
|
||||||
|
import com.github.diamondminer88.zip.ZipReader
|
||||||
|
import com.github.diamondminer88.zip.ZipWriter
|
||||||
|
import dev.beefers.vendetta.manager.BuildConfig
|
||||||
|
import dev.beefers.vendetta.manager.R
|
||||||
|
import dev.beefers.vendetta.manager.domain.manager.DownloadManager
|
||||||
|
import dev.beefers.vendetta.manager.domain.manager.PreferenceManager
|
||||||
|
import dev.beefers.vendetta.manager.installer.util.ManifestPatcher
|
||||||
|
import dev.beefers.vendetta.manager.installer.util.Patcher
|
||||||
|
import dev.beefers.vendetta.manager.installer.util.installApks
|
||||||
|
import dev.beefers.vendetta.manager.network.utils.Signer
|
||||||
|
import dev.beefers.vendetta.manager.utils.copyText
|
||||||
|
import dev.beefers.vendetta.manager.utils.showToast
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.lsposed.patch.util.Logger
|
||||||
|
import java.io.File
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
import kotlin.time.ExperimentalTime
|
||||||
|
import kotlin.time.measureTimedValue
|
||||||
|
|
||||||
|
class InstallerViewModel(
|
||||||
|
private val context: Context,
|
||||||
|
private val downloadManager: DownloadManager,
|
||||||
|
private val preferences: PreferenceManager
|
||||||
|
) : ScreenModel {
|
||||||
|
private val installationRunning = AtomicBoolean(false)
|
||||||
|
private val cacheDir = context.externalCacheDir!!
|
||||||
|
private var debugInfo = """
|
||||||
|
Vendetta Manager ${BuildConfig.VERSION_NAME}
|
||||||
|
Built from commit ${BuildConfig.GIT_COMMIT} on ${BuildConfig.GIT_BRANCH} ${if (BuildConfig.GIT_LOCAL_CHANGES || BuildConfig.GIT_LOCAL_COMMITS) "(Changes Present)" else ""}
|
||||||
|
|
||||||
|
Running Android ${Build.VERSION.RELEASE}, API level ${Build.VERSION.SDK_INT}
|
||||||
|
Supported ABIs: ${Build.SUPPORTED_ABIS.joinToString()}
|
||||||
|
|
||||||
|
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
private val logger = object : Logger() {
|
||||||
|
override fun d(msg: String?) {
|
||||||
|
if (msg != null) {
|
||||||
|
Log.d("Installer", msg)
|
||||||
|
debugInfo += "$msg\n"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun i(msg: String?) {
|
||||||
|
if (msg != null) {
|
||||||
|
Log.i("Installer", msg)
|
||||||
|
debugInfo += "$msg\n"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun e(msg: String?) {
|
||||||
|
if (msg != null) {
|
||||||
|
Log.e("Installer", msg)
|
||||||
|
debugInfo += "$msg\n"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun copyDebugInfo() {
|
||||||
|
context.copyText(debugInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearCache() {
|
||||||
|
cacheDir.deleteRecursively()
|
||||||
|
context.showToast(R.string.msg_cleared_cache)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val job = coroutineScope.launch(Dispatchers.Main) {
|
||||||
|
if (installationRunning.getAndSet(true)) {
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
install()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun install() {
|
||||||
|
steps += listOf(
|
||||||
|
InstallStep.DL_BASE_APK to InstallStepData(
|
||||||
|
InstallStep.DL_BASE_APK.nameRes,
|
||||||
|
InstallStatus.QUEUED
|
||||||
|
),
|
||||||
|
InstallStep.DL_LIBS_APK to InstallStepData(
|
||||||
|
InstallStep.DL_LIBS_APK.nameRes,
|
||||||
|
InstallStatus.QUEUED
|
||||||
|
),
|
||||||
|
InstallStep.DL_LANG_APK to InstallStepData(
|
||||||
|
InstallStep.DL_LANG_APK.nameRes,
|
||||||
|
InstallStatus.QUEUED
|
||||||
|
),
|
||||||
|
InstallStep.DL_RESC_APK to InstallStepData(
|
||||||
|
InstallStep.DL_RESC_APK.nameRes,
|
||||||
|
InstallStatus.QUEUED
|
||||||
|
),
|
||||||
|
InstallStep.DL_VD to InstallStepData(InstallStep.DL_VD.nameRes, InstallStatus.QUEUED),
|
||||||
|
InstallStep.ADD_VD to InstallStepData(InstallStep.ADD_VD.nameRes, InstallStatus.QUEUED),
|
||||||
|
InstallStep.PATCH_MANIFESTS to InstallStepData(
|
||||||
|
InstallStep.PATCH_MANIFESTS.nameRes,
|
||||||
|
InstallStatus.QUEUED
|
||||||
|
),
|
||||||
|
InstallStep.SIGN_APK to InstallStepData(
|
||||||
|
InstallStep.SIGN_APK.nameRes,
|
||||||
|
InstallStatus.QUEUED
|
||||||
|
),
|
||||||
|
InstallStep.INSTALL_APK to InstallStepData(
|
||||||
|
InstallStep.INSTALL_APK.nameRes,
|
||||||
|
InstallStatus.QUEUED
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (preferences.patchIcon) steps += InstallStep.CHANGE_ICON to InstallStepData(
|
||||||
|
InstallStep.CHANGE_ICON.nameRes,
|
||||||
|
InstallStatus.QUEUED
|
||||||
|
)
|
||||||
|
|
||||||
|
val version = preferences.discordVersion.ifBlank { "168018" }
|
||||||
|
val arch = Build.SUPPORTED_ABIS.first()
|
||||||
|
val discordCacheDir = cacheDir.resolve(version)
|
||||||
|
val patchedDir = discordCacheDir.resolve("patched").also { it.deleteRecursively() }
|
||||||
|
val signedDir = discordCacheDir.resolve("signed").also { it.deleteRecursively() }
|
||||||
|
val lspatchedDir = patchedDir.resolve("lspatched").also { it.deleteRecursively() }
|
||||||
|
|
||||||
|
// Download base.apk
|
||||||
|
val baseApk = step(InstallStep.DL_BASE_APK) {
|
||||||
|
discordCacheDir.resolve("base-$version.apk").let { file ->
|
||||||
|
logger.i("Checking if base-$version.apk is cached")
|
||||||
|
if (file.exists()) {
|
||||||
|
cached = true
|
||||||
|
logger.i("base-$version.apk is cached")
|
||||||
|
} else {
|
||||||
|
logger.i("base-$version.apk is not cached, downloading now")
|
||||||
|
downloadManager.downloadDiscordApk(version, file)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.i("Move base-$version.apk to working directory")
|
||||||
|
file.copyTo(
|
||||||
|
patchedDir.resolve(file.name),
|
||||||
|
true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download libs apk
|
||||||
|
val libsApk = step(InstallStep.DL_LIBS_APK) {
|
||||||
|
val libArch = arch.replace("-v", "_v")
|
||||||
|
discordCacheDir.resolve("config.$libArch-$version.apk").let { file ->
|
||||||
|
logger.i("Checking if config.$libArch-$version.apk is cached")
|
||||||
|
if (file.exists()) {
|
||||||
|
logger.i("config.$libArch-$version.apk is cached")
|
||||||
|
cached = true
|
||||||
|
} else {
|
||||||
|
logger.i("config.$libArch-$version.apk is not cached, downloading now")
|
||||||
|
downloadManager.downloadSplit(
|
||||||
|
version = version,
|
||||||
|
split = "config.$libArch",
|
||||||
|
out = file
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.i("Move config.$libArch-$version.apk to working directory")
|
||||||
|
file.copyTo(
|
||||||
|
patchedDir.resolve(file.name),
|
||||||
|
true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download locale apk
|
||||||
|
val langApk = step(InstallStep.DL_LANG_APK) {
|
||||||
|
discordCacheDir.resolve("config.en-$version.apk").let { file ->
|
||||||
|
logger.i("Checking if config.en-$version.apk is cached")
|
||||||
|
if (file.exists()) {
|
||||||
|
logger.i("config.en-$version.apk is cached")
|
||||||
|
cached = true
|
||||||
|
} else {
|
||||||
|
logger.i("config.en-$version.apk is not cached, downloading now")
|
||||||
|
downloadManager.downloadSplit(
|
||||||
|
version = version,
|
||||||
|
split = "config.en",
|
||||||
|
out = file
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.i("Move config.en-$version.apk to working directory")
|
||||||
|
file.copyTo(
|
||||||
|
patchedDir.resolve(file.name),
|
||||||
|
true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download resources apk
|
||||||
|
val resApk = step(InstallStep.DL_RESC_APK) {
|
||||||
|
discordCacheDir.resolve("config.xxhdpi-$version.apk").let { file ->
|
||||||
|
logger.i("Checking if config.xxhdpi-$version.apk is cached")
|
||||||
|
if (file.exists()) {
|
||||||
|
logger.i("config.xxhdpi-$version.apk is cached")
|
||||||
|
cached = true
|
||||||
|
} else {
|
||||||
|
logger.i("config.xxhdpi-$version.apk is not cached, downloading now")
|
||||||
|
downloadManager.downloadSplit(
|
||||||
|
version = version,
|
||||||
|
split = "config.xxhdpi",
|
||||||
|
out = file
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.i("Move config.xxhdpi-$version.apk to working directory")
|
||||||
|
file.copyTo(
|
||||||
|
patchedDir.resolve(file.name),
|
||||||
|
true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download vendetta apk
|
||||||
|
val vendetta = step(InstallStep.DL_VD) {
|
||||||
|
discordCacheDir.resolve("vendetta.apk").let { file ->
|
||||||
|
logger.i("Checking if vendetta.apk is cached")
|
||||||
|
if (file.exists()) {
|
||||||
|
logger.i("vendetta.apk is cached")
|
||||||
|
cached = true
|
||||||
|
} else {
|
||||||
|
logger.i("vendetta.apk is not cached, downloading now")
|
||||||
|
downloadManager.downloadVendetta(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.i("Move vendetta.apk to working directory")
|
||||||
|
file.copyTo(
|
||||||
|
patchedDir.resolve(file.name),
|
||||||
|
true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preferences.patchIcon) {
|
||||||
|
step(InstallStep.CHANGE_ICON) {
|
||||||
|
ZipWriter(baseApk, true).use { baseApk ->
|
||||||
|
val mipmaps =
|
||||||
|
arrayOf("mipmap-xhdpi-v4", "mipmap-xxhdpi-v4", "mipmap-xxxhdpi-v4")
|
||||||
|
val icons = arrayOf(
|
||||||
|
"ic_logo_foreground.png",
|
||||||
|
"ic_logo_square.png",
|
||||||
|
"ic_logo_foreground.png"
|
||||||
|
)
|
||||||
|
|
||||||
|
for (icon in icons) {
|
||||||
|
val newIcon = context.assets.open("icons/$icon")
|
||||||
|
.use { it.readBytes() }
|
||||||
|
|
||||||
|
for (mipmap in mipmaps) {
|
||||||
|
logger.i("Replacing $mipmap with $icon")
|
||||||
|
val path = "res/$mipmap/$icon"
|
||||||
|
baseApk.deleteEntry(path)
|
||||||
|
baseApk.writeEntry(path, newIcon)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Patch manifests
|
||||||
|
step(InstallStep.PATCH_MANIFESTS) {
|
||||||
|
arrayOf(baseApk, libsApk, langApk, resApk).forEach { apk ->
|
||||||
|
logger.i("Reading AndroidManifest.xml from ${apk!!.name}")
|
||||||
|
val manifest = ZipReader(apk)
|
||||||
|
.use { zip -> zip.openEntry("AndroidManifest.xml")?.read() }
|
||||||
|
?: throw IllegalStateException("No manifest in ${apk.name}")
|
||||||
|
|
||||||
|
ZipWriter(apk, true).use { zip ->
|
||||||
|
logger.i("Changing package and app name in ${apk.name}")
|
||||||
|
val patchedManifestBytes = if (apk == baseApk) {
|
||||||
|
ManifestPatcher.patchManifest(
|
||||||
|
manifestBytes = manifest,
|
||||||
|
packageName = preferences.packageName,
|
||||||
|
appName = preferences.appName,
|
||||||
|
debuggable = preferences.debuggable,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
logger.i("Changing package name in ${apk.name}")
|
||||||
|
ManifestPatcher.renamePackage(manifest, preferences.packageName)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.i("Deleting old AndroidManifest.xml in ${apk.name}")
|
||||||
|
zip.deleteEntry(
|
||||||
|
"AndroidManifest.xml",
|
||||||
|
apk == libsApk
|
||||||
|
) // Preserve alignment in libs apk
|
||||||
|
logger.i("Adding patched AndroidManifest.xml in ${apk.name}")
|
||||||
|
zip.writeEntry("AndroidManifest.xml", patchedManifestBytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
step(InstallStep.SIGN_APK) {
|
||||||
|
// Align resources.arsc due to targeting api 30 for silent install
|
||||||
|
logger.i("Creating dir for signed apks")
|
||||||
|
signedDir.mkdir()
|
||||||
|
val apks = arrayOf(baseApk, libsApk, langApk, resApk)
|
||||||
|
if (Build.VERSION.SDK_INT >= 31) {
|
||||||
|
for (file in apks) {
|
||||||
|
logger.i("Byte aligning ${file!!.name}")
|
||||||
|
val bytes = ZipReader(file).use {
|
||||||
|
if (it.entryNames.contains("resources.arsc")) {
|
||||||
|
it.openEntry("resources.arsc")?.read()
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
} ?: continue
|
||||||
|
|
||||||
|
ZipWriter(file, true).use {
|
||||||
|
logger.i("Removing old resources.arsc")
|
||||||
|
it.deleteEntry("resources.arsc", true)
|
||||||
|
logger.i("Adding aligned resources.arsc")
|
||||||
|
it.writeEntry("resources.arsc", bytes, ZipCompression.NONE, 4096)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apks.forEach {
|
||||||
|
logger.i("Signing ${it!!.name}")
|
||||||
|
Signer.signApk(it, File(signedDir, it.name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
step(InstallStep.ADD_VD) {
|
||||||
|
val files = mutableListOf<File>()
|
||||||
|
signedDir.list { _, name ->
|
||||||
|
files.add(signedDir.resolve(name))
|
||||||
|
}
|
||||||
|
Patcher.patch(
|
||||||
|
logger,
|
||||||
|
outputDir = lspatchedDir,
|
||||||
|
apkPaths = files.map { it.absolutePath },
|
||||||
|
embeddedModules = listOf(vendetta!!.absolutePath)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
step(InstallStep.INSTALL_APK) {
|
||||||
|
logger.i("Gathering final apks")
|
||||||
|
val files = mutableListOf<File>()
|
||||||
|
lspatchedDir.list { _, name ->
|
||||||
|
files.add(lspatchedDir.resolve(name))
|
||||||
|
}
|
||||||
|
logger.i("Installing apks")
|
||||||
|
context.installApks(true, *files.toTypedArray())
|
||||||
|
isFinished = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalTime::class)
|
||||||
|
private inline fun <T> step(step: InstallStep, block: InstallStepData.() -> T): T? {
|
||||||
|
if (isFinished) return null
|
||||||
|
steps[step]!!.status = InstallStatus.ONGOING
|
||||||
|
currentStep = step
|
||||||
|
|
||||||
|
try {
|
||||||
|
val value = measureTimedValue { block.invoke(steps[step]!!) }
|
||||||
|
val millis = value.duration.inWholeMilliseconds
|
||||||
|
|
||||||
|
steps[step]!!.apply {
|
||||||
|
duration = millis.div(1000f)
|
||||||
|
status = InstallStatus.SUCCESSFUL
|
||||||
|
}
|
||||||
|
return value.value
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
steps[step]!!.status = InstallStatus.UNSUCCESSFUL
|
||||||
|
|
||||||
|
logger.e("\nFailed on step ${step.name}\n")
|
||||||
|
logger.e(e.stackTraceToString())
|
||||||
|
|
||||||
|
currentStep = step
|
||||||
|
isFinished = true
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class InstallStepGroup(@StringRes val nameRes: Int) {
|
||||||
|
DL(R.string.group_download),
|
||||||
|
PATCHING(R.string.group_patch),
|
||||||
|
INSTALLING(R.string.group_installing)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class InstallStep(
|
||||||
|
val group: InstallStepGroup,
|
||||||
|
@StringRes val nameRes: Int
|
||||||
|
) {
|
||||||
|
DL_BASE_APK(InstallStepGroup.DL, R.string.step_dl_base),
|
||||||
|
DL_LIBS_APK(InstallStepGroup.DL, R.string.step_dl_lib),
|
||||||
|
DL_LANG_APK(InstallStepGroup.DL, R.string.step_dl_lang),
|
||||||
|
DL_RESC_APK(InstallStepGroup.DL, R.string.step_dl_res),
|
||||||
|
DL_VD(InstallStepGroup.DL, R.string.step_dl_vd),
|
||||||
|
|
||||||
|
CHANGE_ICON(InstallStepGroup.PATCHING, R.string.step_change_icon),
|
||||||
|
PATCH_MANIFESTS(InstallStepGroup.PATCHING, R.string.step_patch_manifests),
|
||||||
|
SIGN_APK(InstallStepGroup.PATCHING, R.string.step_signing),
|
||||||
|
ADD_VD(InstallStepGroup.PATCHING, R.string.step_add_vd),
|
||||||
|
|
||||||
|
INSTALL_APK(InstallStepGroup.INSTALLING, R.string.step_installing)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class InstallStatus {
|
||||||
|
ONGOING,
|
||||||
|
SUCCESSFUL,
|
||||||
|
UNSUCCESSFUL,
|
||||||
|
QUEUED
|
||||||
|
}
|
||||||
|
|
||||||
|
@Stable
|
||||||
|
class InstallStepData(
|
||||||
|
@StringRes val nameRes: Int,
|
||||||
|
status: InstallStatus,
|
||||||
|
duration: Float = 0f,
|
||||||
|
cached: Boolean = false
|
||||||
|
) {
|
||||||
|
var status by mutableStateOf(status)
|
||||||
|
var duration by mutableStateOf(duration)
|
||||||
|
var cached by mutableStateOf(cached)
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentStep by mutableStateOf<InstallStep?>(null)
|
||||||
|
val steps = mutableStateMapOf<InstallStep, InstallStepData>()
|
||||||
|
var isFinished by mutableStateOf(false)
|
||||||
|
|
||||||
|
fun getSteps(group: InstallStepGroup): List<InstallStepData> {
|
||||||
|
return steps
|
||||||
|
.filterKeys { it.group === group }.entries
|
||||||
|
.sortedBy { it.key.ordinal }
|
||||||
|
.map { it.value }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
package dev.beefers.vendetta.manager.ui.viewmodel.main
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import cafe.adriel.voyager.core.model.ScreenModel
|
||||||
|
import cafe.adriel.voyager.core.model.coroutineScope
|
||||||
|
import dev.beefers.vendetta.manager.domain.manager.DownloadManager
|
||||||
|
import dev.beefers.vendetta.manager.domain.repository.GithubRepository
|
||||||
|
import dev.beefers.vendetta.manager.installer.util.installApks
|
||||||
|
import dev.beefers.vendetta.manager.network.dto.Release
|
||||||
|
import dev.beefers.vendetta.manager.network.utils.dataOrNull
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class MainViewModel(
|
||||||
|
private val githubRepo: GithubRepository,
|
||||||
|
private val downloadManager: DownloadManager,
|
||||||
|
private val context: Context
|
||||||
|
) : ScreenModel {
|
||||||
|
private val downloadDir = context.cacheDir
|
||||||
|
var release by mutableStateOf<Release?>(null)
|
||||||
|
private set
|
||||||
|
|
||||||
|
init {
|
||||||
|
checkForUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkForUpdate() {
|
||||||
|
coroutineScope.launch {
|
||||||
|
release = githubRepo.getLatestRelease().dataOrNull
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun downloadAndInstallUpdate() {
|
||||||
|
coroutineScope.launch {
|
||||||
|
val update = File(downloadDir, "update.apk")
|
||||||
|
downloadManager.downloadUpdate(update)
|
||||||
|
context.installApks(false, update)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
package dev.beefers.vendetta.manager.ui.viewmodel.settings
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import cafe.adriel.voyager.core.model.ScreenModel
|
||||||
|
import dev.beefers.vendetta.manager.R
|
||||||
|
import dev.beefers.vendetta.manager.utils.showToast
|
||||||
|
|
||||||
|
class SettingsViewModel(
|
||||||
|
private val context: Context
|
||||||
|
) : ScreenModel {
|
||||||
|
private val cacheDir = context.externalCacheDir!!
|
||||||
|
|
||||||
|
fun clearCache() {
|
||||||
|
cacheDir.deleteRecursively()
|
||||||
|
context.showToast(R.string.msg_cleared_cache)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,137 @@
|
||||||
|
package dev.beefers.vendetta.manager.ui.widgets.installer
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.KeyboardArrowDown
|
||||||
|
import androidx.compose.material.icons.filled.KeyboardArrowUp
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.surfaceColorAtElevation
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import dev.beefers.vendetta.manager.R
|
||||||
|
import dev.beefers.vendetta.manager.ui.viewmodel.installer.InstallerViewModel
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun StepGroupCard(
|
||||||
|
name: String,
|
||||||
|
isCurrent: Boolean,
|
||||||
|
steps: List<InstallerViewModel.InstallStepData>,
|
||||||
|
onClick: () -> Unit
|
||||||
|
) {
|
||||||
|
val status = when {
|
||||||
|
steps.all { it.status == InstallerViewModel.InstallStatus.QUEUED } -> InstallerViewModel.InstallStatus.QUEUED
|
||||||
|
steps.all { it.status == InstallerViewModel.InstallStatus.SUCCESSFUL } -> InstallerViewModel.InstallStatus.SUCCESSFUL
|
||||||
|
steps.any { it.status == InstallerViewModel.InstallStatus.ONGOING } -> InstallerViewModel.InstallStatus.ONGOING
|
||||||
|
else -> InstallerViewModel.InstallStatus.UNSUCCESSFUL
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clip(RoundedCornerShape(16.dp))
|
||||||
|
.run {
|
||||||
|
if (isCurrent) {
|
||||||
|
background(MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp))
|
||||||
|
} else this
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable(onClick = onClick)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp)
|
||||||
|
) {
|
||||||
|
StepIcon(status, 24.dp)
|
||||||
|
|
||||||
|
Text(text = name)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
|
||||||
|
if (status != InstallerViewModel.InstallStatus.ONGOING && status != InstallerViewModel.InstallStatus.QUEUED) {
|
||||||
|
Text(
|
||||||
|
text = "%.2fs".format(steps.map { it.duration }.sum()),
|
||||||
|
style = MaterialTheme.typography.labelMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val (arrow, cd) = when {
|
||||||
|
isCurrent -> Icons.Filled.KeyboardArrowUp to R.string.action_collapse
|
||||||
|
else -> Icons.Filled.KeyboardArrowDown to R.string.action_expand
|
||||||
|
}
|
||||||
|
|
||||||
|
Icon(
|
||||||
|
imageVector = arrow,
|
||||||
|
contentDescription = stringResource(cd)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
AnimatedVisibility(visible = isCurrent) {
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
modifier = Modifier
|
||||||
|
.background(MaterialTheme.colorScheme.background.copy(0.6f))
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp)
|
||||||
|
.padding(start = 4.dp)
|
||||||
|
) {
|
||||||
|
steps.forEach {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
) {
|
||||||
|
StepIcon(it.status, size = 18.dp)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResource(it.nameRes),
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier.weight(1f, true),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (it.status != InstallerViewModel.InstallStatus.ONGOING && it.status != InstallerViewModel.InstallStatus.QUEUED) {
|
||||||
|
if (it.cached) {
|
||||||
|
val style = MaterialTheme.typography.labelSmall.copy(
|
||||||
|
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f),
|
||||||
|
fontStyle = FontStyle.Italic,
|
||||||
|
fontSize = 11.sp
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.installer_cached),
|
||||||
|
style = style,
|
||||||
|
maxLines = 1,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "%.2fs".format(it.duration),
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
maxLines = 1,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,71 @@
|
||||||
|
package dev.beefers.vendetta.manager.ui.widgets.installer
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Cancel
|
||||||
|
import androidx.compose.material.icons.filled.CheckCircle
|
||||||
|
import androidx.compose.material.icons.outlined.Circle
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.LocalContentColor
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.semantics.contentDescription
|
||||||
|
import androidx.compose.ui.semantics.semantics
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import dev.beefers.vendetta.manager.R
|
||||||
|
import dev.beefers.vendetta.manager.ui.viewmodel.installer.InstallerViewModel
|
||||||
|
import kotlin.math.floor
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun StepIcon(
|
||||||
|
status: InstallerViewModel.InstallStatus,
|
||||||
|
size: Dp
|
||||||
|
) {
|
||||||
|
val strokeWidth = Dp(floor(size.value / 10) + 1)
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
when (status) {
|
||||||
|
InstallerViewModel.InstallStatus.ONGOING -> {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
strokeWidth = strokeWidth,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(size)
|
||||||
|
.semantics {
|
||||||
|
contentDescription = context.getString(R.string.status_ongoing)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
InstallerViewModel.InstallStatus.SUCCESSFUL -> {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.CheckCircle,
|
||||||
|
contentDescription = stringResource(R.string.status_successful),
|
||||||
|
tint = Color(0xFF59B463),
|
||||||
|
modifier = Modifier.size(size)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
InstallerViewModel.InstallStatus.UNSUCCESSFUL -> {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Cancel,
|
||||||
|
contentDescription = stringResource(R.string.status_fail),
|
||||||
|
tint = MaterialTheme.colorScheme.error,
|
||||||
|
modifier = Modifier.size(size)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
InstallerViewModel.InstallStatus.QUEUED -> {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Circle,
|
||||||
|
contentDescription = stringResource(R.string.status_queued),
|
||||||
|
tint = LocalContentColor.current.copy(alpha = 0.4f),
|
||||||
|
modifier = Modifier.size(size)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
package dev.beefers.vendetta.manager.ui.widgets.updater
|
||||||
|
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.SystemUpdate
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.FilledTonalButton
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import dev.beefers.vendetta.manager.R
|
||||||
|
import dev.beefers.vendetta.manager.network.dto.Release
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun UpdateDialog(
|
||||||
|
release: Release,
|
||||||
|
onConfirm: () -> Unit
|
||||||
|
) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = {},
|
||||||
|
confirmButton = {
|
||||||
|
FilledTonalButton(onClick = onConfirm) {
|
||||||
|
Text(stringResource(R.string.action_update))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title = {
|
||||||
|
Text(stringResource(R.string.title_update))
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Text(stringResource(R.string.update_description, release.versionName))
|
||||||
|
},
|
||||||
|
icon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.SystemUpdate,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
package dev.beefers.vendetta.manager.utils
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import cafe.adriel.voyager.core.screen.Screen
|
||||||
|
import cafe.adriel.voyager.navigator.Navigator
|
||||||
|
import cafe.adriel.voyager.navigator.tab.Tab
|
||||||
|
import cafe.adriel.voyager.navigator.tab.TabOptions
|
||||||
|
import com.google.accompanist.pager.ExperimentalPagerApi
|
||||||
|
import dev.beefers.vendetta.manager.ui.screen.home.HomeScreen
|
||||||
|
import dev.beefers.vendetta.manager.ui.screen.main.LocalPagerState
|
||||||
|
import dev.beefers.vendetta.manager.ui.screen.settings.SettingsScreen
|
||||||
|
|
||||||
|
enum class MainTab(val tab: ManagerTab) {
|
||||||
|
HOME(HomeScreen()),
|
||||||
|
SETTINGS(SettingsScreen())
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalPagerApi::class)
|
||||||
|
@Composable
|
||||||
|
fun Tab.TabOptions(
|
||||||
|
@StringRes title: Int,
|
||||||
|
selectedIcon: ImageVector,
|
||||||
|
unselectedIcon: ImageVector
|
||||||
|
): TabOptions {
|
||||||
|
val pagerState = LocalPagerState.current
|
||||||
|
val selected = MainTab.values()[pagerState.currentPage].tab == this
|
||||||
|
val selectedIconPainter = rememberVectorPainter(
|
||||||
|
image = selectedIcon
|
||||||
|
)
|
||||||
|
val unelectedIconPainter = rememberVectorPainter(
|
||||||
|
image = unselectedIcon
|
||||||
|
)
|
||||||
|
|
||||||
|
return TabOptions(
|
||||||
|
0u,
|
||||||
|
stringResource(title),
|
||||||
|
if (selected) selectedIconPainter else unelectedIconPainter
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
tailrec fun Navigator.navigate(screen: Screen) {
|
||||||
|
if (level == 0)
|
||||||
|
push(screen)
|
||||||
|
else
|
||||||
|
this.parent!!.navigate(screen)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ManagerTab : Tab {
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Actions()
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
package dev.beefers.vendetta.manager.utils
|
||||||
|
|
||||||
|
import android.content.ClipData
|
||||||
|
import android.content.ClipboardManager
|
||||||
|
import android.content.Context
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import dev.beefers.vendetta.manager.BuildConfig
|
||||||
|
|
||||||
|
fun Context.copyText(text: String) {
|
||||||
|
val clipboardManager = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
|
|
||||||
|
clipboardManager.setPrimaryClip(ClipData.newPlainText(BuildConfig.APPLICATION_ID, text))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.showToast(@StringRes res: Int, vararg params: Any, short: Boolean = true) {
|
||||||
|
Toast.makeText(
|
||||||
|
this,
|
||||||
|
getString(res, *params),
|
||||||
|
if (short) Toast.LENGTH_SHORT else Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
package dev.beefers.vendetta.manager.utils
|
||||||
|
|
||||||
|
data class DiscordVersion(
|
||||||
|
val major: Int,
|
||||||
|
val minor: Int,
|
||||||
|
val type: Type
|
||||||
|
) {
|
||||||
|
|
||||||
|
enum class Type {
|
||||||
|
STABLE,
|
||||||
|
BETA,
|
||||||
|
ALPHA
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
fun fromVersionCode(string: String): DiscordVersion? = with(string) {
|
||||||
|
if (length < 4) return@with null
|
||||||
|
if (toIntOrNull() == null) return@with null
|
||||||
|
val codeReversed = toCharArray().reversed().joinToString("")
|
||||||
|
val typeInt = codeReversed[2].toString().toInt()
|
||||||
|
val type = Type.values().getOrNull(typeInt) ?: return@with null
|
||||||
|
DiscordVersion(
|
||||||
|
codeReversed.slice(3..codeReversed.lastIndex).toInt(),
|
||||||
|
codeReversed.substring(0, 2).toInt(),
|
||||||
|
type
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/vendetta_brand" />
|
||||||
|
<foreground android:drawable="@drawable/vendetta_logo" />
|
||||||
|
<monochrome android:drawable="@drawable/vendetta_logo" />
|
||||||
|
</adaptive-icon>
|
|
@ -0,0 +1,18 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="1024dp"
|
||||||
|
android:height="1024dp"
|
||||||
|
android:viewportWidth="1024"
|
||||||
|
android:viewportHeight="1024">
|
||||||
|
<group>
|
||||||
|
<clip-path
|
||||||
|
android:pathData="M0,0h1024v1024h-1024z"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M0,0h1024v1024h-1024z"
|
||||||
|
android:fillColor="#3AB8BA"
|
||||||
|
android:fillAlpha="0"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M520.1,592.7C520,592.7 519.9,592.7 519.8,592.7L519.8,592.7L519.8,592.7L519.8,420.2C519.9,420.2 520,420.2 520.1,420.2L520.1,365.1C520.1,350.3 531.9,338.3 546.5,338.3L585.3,338.3C585.7,339.3 586.7,340.1 587.9,340.1L618.9,340.1L618.9,340.1L618.9,340.1C620.1,340.1 621.1,339.4 621.5,338.3L661.1,338.3C675.6,338.3 687.5,350.3 687.5,365.1L687.5,661C687.5,675.8 675.6,687.8 661.1,687.8L546.5,687.8C532,687.8 520.1,675.8 520.1,661L520.1,592.7ZM525.6,592.3C545.1,590.7 564.1,585.6 581.8,577.3C583.7,578.8 585.6,580.3 587.6,581.6C580.1,586 572.2,589.8 564,592.9C568.3,601.3 573.3,609.4 578.9,617C604.1,609.4 627.7,597.5 648.8,581.9C654.6,522.4 639,470.9 607.8,425.1C589.7,416.8 570.5,410.9 550.9,407.6C548.2,412.4 545.7,417.4 543.6,422.4C537.6,421.5 531.6,420.9 525.6,420.5L525.6,365.2C525.6,353.5 535.2,344 546.9,344L660.7,344C672.4,344 682,353.5 682,365.2L682,659.7C682,671.4 672.4,680.8 660.7,680.8L660.7,680.8L660.7,680.8L546.9,680.8C535.2,680.8 525.6,671.4 525.6,659.7L525.6,592.3ZM441.3,522.8C441.3,538 452.5,550.4 466.2,550.4C480,550.4 490.7,538 491,522.8C491.2,507.6 480.1,495.1 466.1,495.1C452.1,495.1 441.3,507.6 441.3,522.8L441.3,522.8L441.3,522.8ZM533,522.8C533,538 544.2,550.4 557.8,550.4C571.7,550.4 582.4,538 582.7,522.8C582.9,507.6 571.8,495.1 557.8,495.1C543.9,495.1 533,507.6 533,522.8L533,522.8L533,522.8ZM423.8,360.2C426.6,360.2 428.9,357.9 428.9,355.1C428.9,352.3 426.6,350 423.8,350C421,350 418.7,352.3 418.7,355.1C418.7,357.9 421,360.2 423.8,360.2L423.8,360.2L423.8,360.2ZM498.5,420.5C492.4,420.9 486.4,421.5 480.4,422.4C478.2,417.4 475.8,412.4 473.1,407.6C453.4,411 434.2,416.9 416.1,425.2C380.1,478.6 370.3,530.6 375.2,581.9C396.3,597.5 419.9,609.4 445,617C450.7,609.4 455.7,601.3 460,592.9C451.8,589.8 443.9,586 436.4,581.6C438.4,580.2 440.3,578.7 442.2,577.3C459.9,585.6 479,590.7 498.5,592.3L498.5,668.6C498.5,672.7 495.1,676.1 491,676.1L356.6,676.1C352.5,676.1 349.1,672.7 349.1,668.6L349.1,352.4C349.1,348.3 352.5,344.9 356.6,344.9L356.6,344.9L356.6,344.9L491,344.9C495.1,344.9 498.5,348.3 498.5,352.4L498.5,420.5L498.5,420.5ZM504.8,420.2C504.8,420.2 504.8,420.2 504.8,420.2L504.8,420.2L504.8,592.7C504.8,592.7 504.8,592.7 504.8,592.7L504.8,674.6C504.8,681.2 499.4,686.6 492.8,686.6L354.8,686.6C348.2,686.6 342.8,681.2 342.8,674.6L342.8,350C342.8,343.4 348.2,338 354.8,338L354.8,338L354.8,338L492.8,338C499.4,338 504.8,343.4 504.8,350L504.8,420.2L504.8,420.2ZM622.6,349.4L585.2,349.4C581.9,349.4 579.2,352.1 579.2,355.4C579.2,358.7 581.9,361.4 585.2,361.4L622.6,361.4C625.9,361.4 628.6,358.7 628.6,355.4C628.5,352.1 625.9,349.4 622.6,349.4L622.6,349.4Z"
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
</group>
|
||||||
|
</vector>
|
Binary file not shown.
After Width: | Height: | Size: 33 KiB |
|
@ -0,0 +1,18 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="1024dp"
|
||||||
|
android:height="1024dp"
|
||||||
|
android:viewportWidth="1024"
|
||||||
|
android:viewportHeight="1024">
|
||||||
|
<group>
|
||||||
|
<clip-path
|
||||||
|
android:pathData="M0,0h1024v1024h-1024z"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M0,0h1024v1024h-1024z"
|
||||||
|
android:fillColor="#3AB8BA"
|
||||||
|
android:fillAlpha="0"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M520.1,592.7C520,592.7 519.9,592.7 519.8,592.7L519.8,592.7L519.8,592.7L519.8,420.2C519.9,420.2 520,420.2 520.1,420.2L520.1,365.1C520.1,350.3 531.9,338.3 546.5,338.3L585.3,338.3C585.7,339.3 586.7,340.1 587.9,340.1L618.9,340.1L618.9,340.1L618.9,340.1C620.1,340.1 621.1,339.4 621.5,338.3L661.1,338.3C675.6,338.3 687.5,350.3 687.5,365.1L687.5,661C687.5,675.8 675.6,687.8 661.1,687.8L546.5,687.8C532,687.8 520.1,675.8 520.1,661L520.1,592.7ZM525.6,592.3C545.1,590.7 564.1,585.6 581.8,577.3C583.7,578.8 585.6,580.3 587.6,581.6C580.1,586 572.2,589.8 564,592.9C568.3,601.3 573.3,609.4 578.9,617C604.1,609.4 627.7,597.5 648.8,581.9C654.6,522.4 639,470.9 607.8,425.1C589.7,416.8 570.5,410.9 550.9,407.6C548.2,412.4 545.7,417.4 543.6,422.4C537.6,421.5 531.6,420.9 525.6,420.5L525.6,365.2C525.6,353.5 535.2,344 546.9,344L660.7,344C672.4,344 682,353.5 682,365.2L682,659.7C682,671.4 672.4,680.8 660.7,680.8L660.7,680.8L660.7,680.8L546.9,680.8C535.2,680.8 525.6,671.4 525.6,659.7L525.6,592.3ZM441.3,522.8C441.3,538 452.5,550.4 466.2,550.4C480,550.4 490.7,538 491,522.8C491.2,507.6 480.1,495.1 466.1,495.1C452.1,495.1 441.3,507.6 441.3,522.8L441.3,522.8L441.3,522.8ZM533,522.8C533,538 544.2,550.4 557.8,550.4C571.7,550.4 582.4,538 582.7,522.8C582.9,507.6 571.8,495.1 557.8,495.1C543.9,495.1 533,507.6 533,522.8L533,522.8L533,522.8ZM423.8,360.2C426.6,360.2 428.9,357.9 428.9,355.1C428.9,352.3 426.6,350 423.8,350C421,350 418.7,352.3 418.7,355.1C418.7,357.9 421,360.2 423.8,360.2L423.8,360.2L423.8,360.2ZM498.5,420.5C492.4,420.9 486.4,421.5 480.4,422.4C478.2,417.4 475.8,412.4 473.1,407.6C453.4,411 434.2,416.9 416.1,425.2C380.1,478.6 370.3,530.6 375.2,581.9C396.3,597.5 419.9,609.4 445,617C450.7,609.4 455.7,601.3 460,592.9C451.8,589.8 443.9,586 436.4,581.6C438.4,580.2 440.3,578.7 442.2,577.3C459.9,585.6 479,590.7 498.5,592.3L498.5,668.6C498.5,672.7 495.1,676.1 491,676.1L356.6,676.1C352.5,676.1 349.1,672.7 349.1,668.6L349.1,352.4C349.1,348.3 352.5,344.9 356.6,344.9L356.6,344.9L356.6,344.9L491,344.9C495.1,344.9 498.5,348.3 498.5,352.4L498.5,420.5L498.5,420.5ZM504.8,420.2C504.8,420.2 504.8,420.2 504.8,420.2L504.8,420.2L504.8,592.7C504.8,592.7 504.8,592.7 504.8,592.7L504.8,674.6C504.8,681.2 499.4,686.6 492.8,686.6L354.8,686.6C348.2,686.6 342.8,681.2 342.8,674.6L342.8,350C342.8,343.4 348.2,338 354.8,338L354.8,338L354.8,338L492.8,338C499.4,338 504.8,343.4 504.8,350L504.8,420.2L504.8,420.2ZM622.6,349.4L585.2,349.4C581.9,349.4 579.2,352.1 579.2,355.4C579.2,358.7 581.9,361.4 585.2,361.4L622.6,361.4C625.9,361.4 628.6,358.7 628.6,355.4C628.5,352.1 625.9,349.4 622.6,349.4L622.6,349.4Z"
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
</group>
|
||||||
|
</vector>
|
|
@ -0,0 +1,11 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="purple_200">#FFBB86FC</color>
|
||||||
|
<color name="purple_500">#FF6200EE</color>
|
||||||
|
<color name="purple_700">#FF3700B3</color>
|
||||||
|
<color name="teal_200">#FF03DAC5</color>
|
||||||
|
<color name="teal_700">#FF018786</color>
|
||||||
|
<color name="black">#FF000000</color>
|
||||||
|
<color name="white">#FFFFFFFF</color>
|
||||||
|
<color name="vendetta_brand">#FF3AB8BA</color>
|
||||||
|
</resources>
|
|
@ -0,0 +1,63 @@
|
||||||
|
<resources>
|
||||||
|
<string name="app_name">Vendetta Manager</string>
|
||||||
|
|
||||||
|
<string name="msg_cleared_cache">Cache cleared successfully</string>
|
||||||
|
|
||||||
|
<string name="group_download">Download APKs</string>
|
||||||
|
<string name="group_patch">Patching</string>
|
||||||
|
<string name="group_installing">Installing</string>
|
||||||
|
|
||||||
|
<string name="step_dl_base">Downloading base apk</string>
|
||||||
|
<string name="step_dl_lib">Downloading libraries apk</string>
|
||||||
|
<string name="step_dl_lang">Downloading language apk</string>
|
||||||
|
<string name="step_dl_res">Downloading resources apk</string>
|
||||||
|
<string name="step_dl_vd">Downloading Vendetta module</string>
|
||||||
|
|
||||||
|
<string name="step_change_icon">Changing app icon</string>
|
||||||
|
<string name="step_patch_manifests">Patching app manifests</string>
|
||||||
|
<string name="step_add_vd">Injecting Vendetta</string>
|
||||||
|
|
||||||
|
<string name="step_signing">Signing APKs</string>
|
||||||
|
<string name="step_installing">Installing APKs</string>
|
||||||
|
|
||||||
|
<string name="installer_success">Installed successfully</string>
|
||||||
|
<string name="installer_aborted">Install canceled</string>
|
||||||
|
<string name="installer_failed">Failed to install: %d</string>
|
||||||
|
|
||||||
|
<string name="status_ongoing">Ongoing</string>
|
||||||
|
<string name="status_successful">Successful</string>
|
||||||
|
<string name="status_fail">Failed</string>
|
||||||
|
<string name="status_queued">Queued</string>
|
||||||
|
|
||||||
|
<string name="action_collapse">Collapse</string>
|
||||||
|
<string name="action_expand">Expand</string>
|
||||||
|
<string name="action_back">Go back</string>
|
||||||
|
<string name="action_copy_logs">Copy logs</string>
|
||||||
|
<string name="action_clear_cache">Clear cache</string>
|
||||||
|
<string name="action_confirm">Confirm</string>
|
||||||
|
<string name="action_update">Start update</string>
|
||||||
|
<string name="action_install">Install</string>
|
||||||
|
|
||||||
|
<string name="installer_cached">Cached</string>
|
||||||
|
|
||||||
|
<string name="title_installer">Installer</string>
|
||||||
|
<string name="title_home">Home</string>
|
||||||
|
<string name="title_settings">Settings</string>
|
||||||
|
<string name="title_update">Update available!</string>
|
||||||
|
|
||||||
|
<string name="theme_system">System</string>
|
||||||
|
<string name="theme_light">Light</string>
|
||||||
|
<string name="theme_dark">Dark</string>
|
||||||
|
|
||||||
|
<string name="settings_appearance">Appearance</string>
|
||||||
|
<string name="settings_dynamic_color">Dynamic color</string>
|
||||||
|
<string name="settings_dynamic_color_description">Only available on Android 12 and up</string>
|
||||||
|
<string name="settings_theme">Theme</string>
|
||||||
|
|
||||||
|
<string name="settings_advanced">Advanced</string>
|
||||||
|
<string name="settings_app_name">App name</string>
|
||||||
|
<string name="settings_app_icon">Replace app icon</string>
|
||||||
|
<string name="settings_app_icon_description">Uses the Vendetta icon instead of Discord\'s</string>
|
||||||
|
|
||||||
|
<string name="update_description">Vendetta Manager version %1$s is now available!</string>
|
||||||
|
</resources>
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
|
||||||
|
<style name="Theme.VendettaManager" parent="android:Theme.Material.Light.NoActionBar" />
|
||||||
|
</resources>
|
|
@ -0,0 +1,13 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?><!--
|
||||||
|
Sample backup rules file; uncomment and customize as necessary.
|
||||||
|
See https://developer.android.com/guide/topics/data/autobackup
|
||||||
|
for details.
|
||||||
|
Note: This file is ignored for devices older that API 31
|
||||||
|
See https://developer.android.com/about/versions/12/backup-restore
|
||||||
|
-->
|
||||||
|
<full-backup-content>
|
||||||
|
<!--
|
||||||
|
<include domain="sharedpref" path="."/>
|
||||||
|
<exclude domain="sharedpref" path="device.xml"/>
|
||||||
|
-->
|
||||||
|
</full-backup-content>
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?><!--
|
||||||
|
Sample data extraction rules file; uncomment and customize as necessary.
|
||||||
|
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
|
||||||
|
for details.
|
||||||
|
-->
|
||||||
|
<data-extraction-rules>
|
||||||
|
<cloud-backup>
|
||||||
|
<!-- TODO: Use <include> and <exclude> to control what is backed up.
|
||||||
|
<include .../>
|
||||||
|
<exclude .../>
|
||||||
|
-->
|
||||||
|
</cloud-backup>
|
||||||
|
<!--
|
||||||
|
<device-transfer>
|
||||||
|
<include .../>
|
||||||
|
<exclude .../>
|
||||||
|
</device-transfer>
|
||||||
|
-->
|
||||||
|
</data-extraction-rules>
|
|
@ -0,0 +1,17 @@
|
||||||
|
package dev.beefers.vendetta.manager
|
||||||
|
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
import org.junit.Assert.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example local unit test, which will execute on the development machine (host).
|
||||||
|
*
|
||||||
|
* See [testing documentation](http://d.android.com/tools/testing).
|
||||||
|
*/
|
||||||
|
class ExampleUnitTest {
|
||||||
|
@Test
|
||||||
|
fun addition_isCorrect() {
|
||||||
|
assertEquals(4, 2 + 2)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
|
plugins {
|
||||||
|
id("com.android.application") version "8.1.0-alpha02" apply false
|
||||||
|
id("com.android.library") version "8.1.0-alpha02" apply false
|
||||||
|
kotlin("android") version "1.7.20" apply false
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.withType<Copy>().all {
|
||||||
|
duplicatesStrategy = DuplicatesStrategy.INCLUDE
|
||||||
|
}
|
||||||
|
|
||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
maven("https://maven.aliucord.com/snapshots")
|
||||||
|
maven("https://jitpack.io")
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
# Project-wide Gradle settings.
|
||||||
|
# IDE (e.g. Android Studio) users:
|
||||||
|
# Gradle settings configured through the IDE *will override*
|
||||||
|
# any settings specified in this file.
|
||||||
|
# For more details on how to configure your build environment visit
|
||||||
|
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||||
|
# Specifies the JVM arguments used for the daemon process.
|
||||||
|
# The setting is particularly useful for tweaking memory settings.
|
||||||
|
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||||
|
# When configured, Gradle will run in incubating parallel mode.
|
||||||
|
# This option should only be used with decoupled projects. More details, visit
|
||||||
|
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||||
|
# org.gradle.parallel=true
|
||||||
|
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||||
|
# Android operating system, and which are packaged with your app's APK
|
||||||
|
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||||
|
android.useAndroidX=true
|
||||||
|
# Kotlin code style for this project: "official" or "obsolete":
|
||||||
|
kotlin.code.style=official
|
||||||
|
# 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,
|
||||||
|
# thereby reducing the size of the R class for that library
|
||||||
|
android.nonTransitiveRClass=true
|
Binary file not shown.
|
@ -0,0 +1,6 @@
|
||||||
|
#Tue Mar 14 14:35:22 EDT 2023
|
||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-rc-1-bin.zip
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
|
@ -0,0 +1,185 @@
|
||||||
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
|
#
|
||||||
|
# Copyright 2015 the original author or authors.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
##
|
||||||
|
## Gradle start up script for UN*X
|
||||||
|
##
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Attempt to set APP_HOME
|
||||||
|
# Resolve links: $0 may be a link
|
||||||
|
PRG="$0"
|
||||||
|
# Need this for relative symlinks.
|
||||||
|
while [ -h "$PRG" ] ; do
|
||||||
|
ls=`ls -ld "$PRG"`
|
||||||
|
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||||
|
if expr "$link" : '/.*' > /dev/null; then
|
||||||
|
PRG="$link"
|
||||||
|
else
|
||||||
|
PRG=`dirname "$PRG"`"/$link"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
SAVED="`pwd`"
|
||||||
|
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||||
|
APP_HOME="`pwd -P`"
|
||||||
|
cd "$SAVED" >/dev/null
|
||||||
|
|
||||||
|
APP_NAME="Gradle"
|
||||||
|
APP_BASE_NAME=`basename "$0"`
|
||||||
|
|
||||||
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
|
MAX_FD="maximum"
|
||||||
|
|
||||||
|
warn () {
|
||||||
|
echo "$*"
|
||||||
|
}
|
||||||
|
|
||||||
|
die () {
|
||||||
|
echo
|
||||||
|
echo "$*"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# OS specific support (must be 'true' or 'false').
|
||||||
|
cygwin=false
|
||||||
|
msys=false
|
||||||
|
darwin=false
|
||||||
|
nonstop=false
|
||||||
|
case "`uname`" in
|
||||||
|
CYGWIN* )
|
||||||
|
cygwin=true
|
||||||
|
;;
|
||||||
|
Darwin* )
|
||||||
|
darwin=true
|
||||||
|
;;
|
||||||
|
MINGW* )
|
||||||
|
msys=true
|
||||||
|
;;
|
||||||
|
NONSTOP* )
|
||||||
|
nonstop=true
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
# Determine the Java command to use to start the JVM.
|
||||||
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
|
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||||
|
else
|
||||||
|
JAVACMD="$JAVA_HOME/bin/java"
|
||||||
|
fi
|
||||||
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD="java"
|
||||||
|
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Increase the maximum file descriptors if we can.
|
||||||
|
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||||
|
MAX_FD_LIMIT=`ulimit -H -n`
|
||||||
|
if [ $? -eq 0 ] ; then
|
||||||
|
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||||
|
MAX_FD="$MAX_FD_LIMIT"
|
||||||
|
fi
|
||||||
|
ulimit -n $MAX_FD
|
||||||
|
if [ $? -ne 0 ] ; then
|
||||||
|
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# For Darwin, add options to specify how the application appears in the dock
|
||||||
|
if $darwin; then
|
||||||
|
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
|
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||||
|
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||||
|
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||||
|
|
||||||
|
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||||
|
|
||||||
|
# We build the pattern for arguments to be converted via cygpath
|
||||||
|
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||||
|
SEP=""
|
||||||
|
for dir in $ROOTDIRSRAW ; do
|
||||||
|
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||||
|
SEP="|"
|
||||||
|
done
|
||||||
|
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||||
|
# Add a user-defined pattern to the cygpath arguments
|
||||||
|
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||||
|
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||||
|
fi
|
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||||
|
i=0
|
||||||
|
for arg in "$@" ; do
|
||||||
|
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||||
|
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||||
|
|
||||||
|
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||||
|
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||||
|
else
|
||||||
|
eval `echo args$i`="\"$arg\""
|
||||||
|
fi
|
||||||
|
i=`expr $i + 1`
|
||||||
|
done
|
||||||
|
case $i in
|
||||||
|
0) set -- ;;
|
||||||
|
1) set -- "$args0" ;;
|
||||||
|
2) set -- "$args0" "$args1" ;;
|
||||||
|
3) set -- "$args0" "$args1" "$args2" ;;
|
||||||
|
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||||
|
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||||
|
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||||
|
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||||
|
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||||
|
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Escape application args
|
||||||
|
save () {
|
||||||
|
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||||
|
echo " "
|
||||||
|
}
|
||||||
|
APP_ARGS=`save "$@"`
|
||||||
|
|
||||||
|
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||||
|
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||||
|
|
||||||
|
exec "$JAVACMD" "$@"
|
|
@ -0,0 +1,89 @@
|
||||||
|
@rem
|
||||||
|
@rem Copyright 2015 the original author or authors.
|
||||||
|
@rem
|
||||||
|
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@rem you may not use this file except in compliance with the License.
|
||||||
|
@rem You may obtain a copy of the License at
|
||||||
|
@rem
|
||||||
|
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
@rem
|
||||||
|
@rem Unless required by applicable law or agreed to in writing, software
|
||||||
|
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
@rem See the License for the specific language governing permissions and
|
||||||
|
@rem limitations under the License.
|
||||||
|
@rem
|
||||||
|
|
||||||
|
@if "%DEBUG%" == "" @echo off
|
||||||
|
@rem ##########################################################################
|
||||||
|
@rem
|
||||||
|
@rem Gradle startup script for Windows
|
||||||
|
@rem
|
||||||
|
@rem ##########################################################################
|
||||||
|
|
||||||
|
@rem Set local scope for the variables with windows NT shell
|
||||||
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
|
set DIRNAME=%~dp0
|
||||||
|
if "%DIRNAME%" == "" set DIRNAME=.
|
||||||
|
set APP_BASE_NAME=%~n0
|
||||||
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||||
|
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||||
|
|
||||||
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||||
|
|
||||||
|
@rem Find java.exe
|
||||||
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
|
set JAVA_EXE=java.exe
|
||||||
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
|
if "%ERRORLEVEL%" == "0" goto execute
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:findJavaFromJavaHome
|
||||||
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:execute
|
||||||
|
@rem Setup the command line
|
||||||
|
|
||||||
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
@rem Execute Gradle
|
||||||
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||||
|
|
||||||
|
:end
|
||||||
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||||
|
|
||||||
|
:fail
|
||||||
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
|
rem the _cmd.exe /c_ return code!
|
||||||
|
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||||
|
exit /b 1
|
||||||
|
|
||||||
|
:mainEnd
|
||||||
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
|
:omega
|
|
@ -0,0 +1,10 @@
|
||||||
|
pluginManagement {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
gradlePluginPortal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rootProject.name = "Vendetta Manager"
|
||||||
|
include(":app")
|
Loading…
Reference in New Issue