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