feat: aliu kotlin install (#33)
This commit is contained in:
parent
10da841a3b
commit
335f2ca7a3
18 changed files with 574 additions and 151 deletions
|
|
@ -1,4 +1,4 @@
|
|||
# Aliucord Manager
|
||||
# Aliucord Manager [](https://crowdin.com/project/aliucord-manager)
|
||||
Aliucord Manager - Aliucord Installer rewrite in [Kotlin](https://kotlinlang.org) and [Jetpack Compose](https://developer.android.com/jetpack/compose).
|
||||
|
||||
INFO: This app is not functional yet, if you want to install Aliucord - [read this](https://github.com/Aliucord/Aliucord#-installation)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.aliucord.manager.di
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import com.aliucord.manager.domain.manager.DownloadManager
|
||||
import com.aliucord.manager.domain.manager.PreferencesManager
|
||||
|
|
@ -11,7 +12,7 @@ val managerModule = module {
|
|||
return PreferencesManager(context.getSharedPreferences("preferences", Context.MODE_PRIVATE))
|
||||
}
|
||||
|
||||
fun provideDownloadManager(context: Context) = DownloadManager(context)
|
||||
fun provideDownloadManager(application: Application) = DownloadManager(application)
|
||||
|
||||
singleOf(::providePreferences)
|
||||
singleOf(::provideDownloadManager)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
package com.aliucord.manager.di
|
||||
|
||||
import com.aliucord.manager.domain.repository.AliucordMavenRepository
|
||||
import com.aliucord.manager.domain.repository.GithubRepository
|
||||
import org.koin.core.module.dsl.singleOf
|
||||
import org.koin.dsl.module
|
||||
|
||||
val repositoryModule = module {
|
||||
singleOf(::GithubRepository)
|
||||
singleOf(::AliucordMavenRepository)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
package com.aliucord.manager.di
|
||||
|
||||
import com.aliucord.manager.network.service.GithubService
|
||||
import com.aliucord.manager.network.service.MavenService
|
||||
import org.koin.core.module.dsl.singleOf
|
||||
import org.koin.dsl.module
|
||||
|
||||
val serviceModule = module {
|
||||
singleOf(::GithubService)
|
||||
singleOf(::MavenService)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,6 @@ val viewModelModule = module {
|
|||
viewModelOf(::HomeViewModel)
|
||||
viewModelOf(::PluginsViewModel)
|
||||
viewModelOf(::AboutViewModel)
|
||||
viewModel { params -> InstallViewModel(get(), get(), get(), get(), params.get()) }
|
||||
viewModelOf(::InstallViewModel)
|
||||
viewModelOf(::SettingsViewModel)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,27 +1,41 @@
|
|||
package com.aliucord.manager.domain.manager
|
||||
|
||||
import android.app.Application
|
||||
import android.app.DownloadManager
|
||||
import android.content.*
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.core.net.toUri
|
||||
import com.aliucord.manager.BuildConfig
|
||||
import com.aliucord.manager.R
|
||||
import com.aliucord.manager.domain.repository.AliucordMavenRepository
|
||||
import com.aliucord.manager.network.service.GithubService
|
||||
import java.io.File
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
import kotlin.coroutines.*
|
||||
|
||||
class DownloadManager(
|
||||
private val context: Context
|
||||
private val application: Application
|
||||
) {
|
||||
private val downloadManager = context.getSystemService<DownloadManager>()!!
|
||||
private val downloadManager = application.getSystemService<DownloadManager>()!!
|
||||
|
||||
suspend fun downloadDiscordApk(version: String): File {
|
||||
return download("$BACKEND_HOST/download/discord?v=$version", "base-$version.apk")
|
||||
}
|
||||
// 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): File {
|
||||
return download("$BACKEND_HOST/download/discord?v=$version&split=$split", "$split-$version.apk")
|
||||
}
|
||||
suspend fun downloadSplit(version: String, split: String, out: File): File =
|
||||
download("$BACKEND_HOST/download/discord?v=$version&split=$split", out)
|
||||
|
||||
suspend fun download(url: String, fileName: String): File {
|
||||
// Aliucord Kotlin downloads
|
||||
suspend fun downloadKtInjector(out: File): File =
|
||||
download(GithubService.KT_INJECTOR_URL, out)
|
||||
|
||||
suspend fun downloadAliuhook(version: String, out: File): File =
|
||||
download(AliucordMavenRepository.getAliuhookUrl(version), out)
|
||||
|
||||
suspend fun downloadKotlinDex(out: File): File =
|
||||
download(GithubService.KOTLIN_DEX_URL, out)
|
||||
|
||||
suspend fun download(url: String, out: File): File {
|
||||
return suspendCoroutine { continuation ->
|
||||
val receiver = object : BroadcastReceiver() {
|
||||
var downloadId = 0L
|
||||
|
|
@ -29,23 +43,60 @@ class DownloadManager(
|
|||
override fun onReceive(context: Context, intent: Intent) {
|
||||
val id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1)
|
||||
|
||||
if (downloadId == id) {
|
||||
context.unregisterReceiver(this)
|
||||
continuation.resume(context.externalCacheDir!!.resolve(fileName))
|
||||
if (downloadId != id) return
|
||||
|
||||
val (status, reason) = DownloadManager.Query().run {
|
||||
setFilterById(downloadId)
|
||||
|
||||
val cursor = downloadManager.query(this)
|
||||
.apply { moveToFirst() }
|
||||
|
||||
val status = cursor.run { getInt(getColumnIndex(DownloadManager.COLUMN_STATUS)) }
|
||||
val reason = cursor.run { getInt(getColumnIndex(DownloadManager.COLUMN_REASON)) }
|
||||
status to reason
|
||||
}
|
||||
|
||||
when (status) {
|
||||
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))
|
||||
application.registerReceiver(receiver, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE))
|
||||
|
||||
receiver.downloadId = DownloadManager.Request(url.toUri()).run {
|
||||
setNotificationVisibility(DownloadManager.Request.VISIBILITY_HIDDEN)
|
||||
setDestinationUri(context.externalCacheDir!!.resolve(fileName).toUri())
|
||||
setTitle("Aliucord Manager: Downloading ${out.name}")
|
||||
setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE)
|
||||
setDestinationUri(out.toUri())
|
||||
addRequestHeader("User-Agent", "Aliucord 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,29 @@
|
|||
package com.aliucord.manager.domain.repository
|
||||
|
||||
import com.aliucord.manager.network.service.MavenService
|
||||
import com.aliucord.manager.network.utils.ApiError
|
||||
import com.aliucord.manager.network.utils.ApiResponse
|
||||
import com.aliucord.manager.network.utils.transform
|
||||
import io.ktor.http.*
|
||||
|
||||
class AliucordMavenRepository(
|
||||
private val maven: MavenService
|
||||
) {
|
||||
suspend fun getAliuhookVersion(): ApiResponse<String> {
|
||||
return maven.getArtifactMetadata(BASE_URL, ALIUHOOK).transform {
|
||||
"<release>(.+?)</release>".toRegex()
|
||||
.find(it)
|
||||
?.groupValues?.get(1)
|
||||
?: return ApiResponse.Error(ApiError(HttpStatusCode.OK, "No version in the aliuhook artifact metadata"))
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val BASE_URL = "https://maven.aliucord.com/snapshots"
|
||||
private const val ALIUHOOK = "com.aliucord:Aliuhook"
|
||||
|
||||
fun getAliuhookUrl(version: String): String {
|
||||
return MavenService.getAARUrl(BASE_URL, "$ALIUHOOK:$version")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -12,7 +12,7 @@ class GithubRepository(
|
|||
suspend fun getContributors() = service.getContributors()
|
||||
.transform { it.sortedByDescending(GithubUser::contributions) }
|
||||
|
||||
suspend fun getVersion() = service.getVersion()
|
||||
suspend fun getDiscordKtVersion() = service.getVersion()
|
||||
|
||||
suspend fun getHermesReleases() = service.getHermesReleases()
|
||||
|
||||
|
|
|
|||
|
|
@ -53,6 +53,8 @@ class GithubService(
|
|||
private const val CONTRIBUTORS_URL = "https://api.github.com/repos/$ORG/$REPO/contributors"
|
||||
|
||||
private const val DATA_URL = "https://raw.githubusercontent.com/$ORG/$REPO/builds/data.json"
|
||||
const val KT_INJECTOR_URL = "https://raw.githubusercontent.com/$ORG/$REPO/builds/Injector.dex"
|
||||
const val KOTLIN_DEX_URL = "https://raw.githubusercontent.com/$ORG/$REPO/main/installer/android/app/src/main/assets/kotlin/classes.dex"
|
||||
|
||||
private const val HERMES_RELEASES_URL = "https://api.github.com/repos/$ORG/$HERMES_REPO/releases"
|
||||
private const val ALIUCORD_NATIVE_RELEASES_URL = "https://api.github.com/repos/$ORG/$NATIVE_REPO/releases"
|
||||
|
|
|
|||
|
|
@ -2,9 +2,7 @@ package com.aliucord.manager.network.service
|
|||
|
||||
import android.util.Log
|
||||
import com.aliucord.manager.BuildConfig
|
||||
import com.aliucord.manager.network.utils.ApiError
|
||||
import com.aliucord.manager.network.utils.ApiFailure
|
||||
import com.aliucord.manager.network.utils.ApiResponse
|
||||
import com.aliucord.manager.network.utils.*
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.client.statement.*
|
||||
|
|
@ -25,6 +23,10 @@ class HttpService(
|
|||
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 {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
package com.aliucord.manager.network.service
|
||||
|
||||
import com.aliucord.manager.network.utils.ApiResponse
|
||||
import io.ktor.client.request.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class MavenService(
|
||||
private val http: HttpService,
|
||||
) {
|
||||
suspend fun getArtifactMetadata(baseUrl: String, artifactCoords: String): ApiResponse<String> = withContext(Dispatchers.IO) {
|
||||
http.request {
|
||||
val coords = artifactCoords.replace("[:.]".toRegex(), "/")
|
||||
url("$baseUrl/${coords}/maven-metadata.xml")
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun getAARUrl(baseUrl: String, coords: String): String {
|
||||
val parts = coords.split("[:.]".toRegex())
|
||||
val artifact = parts[parts.lastIndex - 1]
|
||||
val version = parts.last()
|
||||
return "$baseUrl/${parts.joinToString("/")}/$artifact-$version.aar"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -46,3 +46,30 @@ inline fun <T, R> ApiResponse<T>.transform(block: (T) -> R): ApiResponse<R> {
|
|||
ApiResponse.Success(block(data))
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <T> ApiResponse<T>.getOrThrow(): T {
|
||||
return fold(
|
||||
success = { it },
|
||||
fail = { throw it }
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
inline fun <T, R> ApiResponse<T>.chain(block: (T) -> ApiResponse<R>): ApiResponse<R> {
|
||||
return if (this !is ApiResponse.Success) {
|
||||
// Error and Failure do not use the generic value
|
||||
this as ApiResponse<R>
|
||||
} else {
|
||||
block(data)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
inline fun <T, R> ApiResponse<T>.chain(secondary: ApiResponse<R>): ApiResponse<R> {
|
||||
return if (secondary is ApiResponse.Success) {
|
||||
secondary
|
||||
} else {
|
||||
// Error and Failure do not use the generic value
|
||||
this as ApiResponse<R>
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@
|
|||
|
||||
package com.aliucord.manager.ui.dialog
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Download
|
||||
|
|
@ -14,83 +13,120 @@ import androidx.compose.runtime.*
|
|||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
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.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import com.aliucord.manager.R
|
||||
import com.aliucord.manager.ui.screen.InstallData
|
||||
|
||||
enum class DownloadMethod { DOWNLOAD, SELECT }
|
||||
enum class DownloadMethod { DOWNLOAD }
|
||||
enum class DiscordType { KOTLIN, REACT_NATIVE }
|
||||
|
||||
@Composable
|
||||
fun InstallerDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
onConfirm: (InstallData) -> Unit
|
||||
) {
|
||||
var selectedMethod by rememberSaveable { mutableStateOf(DownloadMethod.DOWNLOAD) }
|
||||
val downloadMethod by rememberSaveable { mutableStateOf(DownloadMethod.DOWNLOAD) }
|
||||
var discordType by rememberSaveable { mutableStateOf<DiscordType?>(null) }
|
||||
|
||||
AlertDialog(
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Download,
|
||||
contentDescription = stringResource(R.string.selector_download_method)
|
||||
fun triggerConfirm() {
|
||||
onDismiss()
|
||||
onConfirm(
|
||||
InstallData(
|
||||
downloadMethod,
|
||||
discordType ?: return
|
||||
)
|
||||
},
|
||||
title = {
|
||||
Text(stringResource(R.string.selector_download_method))
|
||||
},
|
||||
text = {
|
||||
Column {
|
||||
mapOf(
|
||||
DownloadMethod.DOWNLOAD to stringResource(R.string.selector_download_apk),
|
||||
// DownloadMethod.SELECT to stringResource(R.string.select_apk)
|
||||
).forEach { (method, name) ->
|
||||
Column {
|
||||
Row(
|
||||
modifier = Modifier.clickable { selectedMethod = method },
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
)
|
||||
}
|
||||
|
||||
Dialog(
|
||||
onDismissRequest = onDismiss,
|
||||
) {
|
||||
Surface(
|
||||
color = AlertDialogDefaults.containerColor,
|
||||
tonalElevation = AlertDialogDefaults.TonalElevation,
|
||||
shape = AlertDialogDefaults.shape,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.sizeIn(minWidth = 380.dp)
|
||||
.padding(24.dp)
|
||||
) {
|
||||
// Icon
|
||||
Box(
|
||||
Modifier
|
||||
.align(Alignment.CenterHorizontally)
|
||||
.padding(bottom = 16.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Download,
|
||||
tint = AlertDialogDefaults.iconContentColor,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
|
||||
// Title
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterHorizontally)
|
||||
.padding(bottom = 30.dp)
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.selector_discord_type),
|
||||
color = AlertDialogDefaults.titleContentColor,
|
||||
style = MaterialTheme.typography.headlineSmall
|
||||
)
|
||||
}
|
||||
|
||||
// Body
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(bottom = 30.dp)
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.selector_discord_type_body),
|
||||
textAlign = TextAlign.Center,
|
||||
color = AlertDialogDefaults.textContentColor,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
|
||||
// Buttons
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
modifier = Modifier.clip(MaterialTheme.shapes.large)
|
||||
) {
|
||||
ProvideTextStyle(MaterialTheme.typography.labelMedium) {
|
||||
Button(
|
||||
shape = MaterialTheme.shapes.extraSmall,
|
||||
onClick = {
|
||||
discordType = DiscordType.REACT_NATIVE
|
||||
triggerConfirm()
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(50.dp)
|
||||
) {
|
||||
Text(
|
||||
text = name,
|
||||
style = MaterialTheme.typography.labelLarge
|
||||
)
|
||||
|
||||
Spacer(Modifier.weight(1f, true))
|
||||
|
||||
RadioButton(
|
||||
selected = selectedMethod == method,
|
||||
onClick = { selectedMethod = method }
|
||||
)
|
||||
Text(stringResource(R.string.selector_discord_type_kotlin))
|
||||
}
|
||||
Button(
|
||||
shape = MaterialTheme.shapes.extraSmall,
|
||||
onClick = {
|
||||
discordType = DiscordType.KOTLIN
|
||||
triggerConfirm()
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(50.dp)
|
||||
) {
|
||||
Text(stringResource(R.string.selector_discord_type_old))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
Button(
|
||||
onClick = {
|
||||
onConfirm(
|
||||
InstallData(
|
||||
selectedMethod,
|
||||
DiscordType.REACT_NATIVE
|
||||
)
|
||||
)
|
||||
onDismissRequest()
|
||||
}
|
||||
) {
|
||||
Text(stringResource(R.string.action_confirm))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
Button(
|
||||
onClick = onDismissRequest,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
contentColor = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||
containerColor = MaterialTheme.colorScheme.secondaryContainer
|
||||
)
|
||||
) {
|
||||
Text(stringResource(R.string.action_dismiss))
|
||||
}
|
||||
},
|
||||
onDismissRequest = onDismissRequest
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,15 +6,13 @@
|
|||
|
||||
package com.aliucord.manager.ui.screen
|
||||
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.aliucord.manager.ui.component.home.CommitList
|
||||
import com.aliucord.manager.ui.component.home.InfoCard
|
||||
import com.aliucord.manager.ui.dialog.*
|
||||
import com.aliucord.manager.ui.dialog.InstallerDialog
|
||||
import com.aliucord.manager.ui.viewmodel.HomeViewModel
|
||||
import org.koin.androidx.compose.getViewModel
|
||||
|
||||
|
|
@ -23,27 +21,14 @@ fun HomeScreen(
|
|||
viewModel: HomeViewModel = getViewModel(),
|
||||
onClickInstall: (InstallData) -> Unit
|
||||
) {
|
||||
var showOptionsDialog by remember { mutableStateOf(false) }
|
||||
|
||||
if (showOptionsDialog) {
|
||||
val filePicker = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
|
||||
if (uri == null) return@rememberLauncherForActivityResult
|
||||
|
||||
onClickInstall(
|
||||
InstallData(
|
||||
DownloadMethod.SELECT,
|
||||
DiscordType.REACT_NATIVE
|
||||
)
|
||||
)
|
||||
}
|
||||
var showInstallerDialog by remember { mutableStateOf(false) }
|
||||
|
||||
if (showInstallerDialog) {
|
||||
InstallerDialog(
|
||||
onDismissRequest = { showOptionsDialog = false },
|
||||
onDismiss = { showInstallerDialog = false },
|
||||
onConfirm = { data ->
|
||||
when (data.downloadMethod) {
|
||||
DownloadMethod.SELECT -> filePicker.launch(arrayOf("application/octet-stream"))
|
||||
DownloadMethod.DOWNLOAD -> onClickInstall(data)
|
||||
}
|
||||
showInstallerDialog = false
|
||||
onClickInstall(data)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -59,18 +44,7 @@ fun HomeScreen(
|
|||
supportedVersion = viewModel.supportedVersion,
|
||||
supportedVersionType = viewModel.supportedVersionType,
|
||||
currentVersion = viewModel.installedVersion,
|
||||
onDownloadClick = {
|
||||
if (viewModel.preferences.devMode) {
|
||||
showOptionsDialog = true
|
||||
} else {
|
||||
onClickInstall(
|
||||
InstallData(
|
||||
DownloadMethod.DOWNLOAD,
|
||||
DiscordType.REACT_NATIVE
|
||||
)
|
||||
)
|
||||
}
|
||||
},
|
||||
onDownloadClick = { showInstallerDialog = true },
|
||||
onLaunchClick = viewModel::launchAliucord,
|
||||
onUninstallClick = viewModel::uninstallAliucord
|
||||
)
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ class HomeViewModel(
|
|||
}
|
||||
|
||||
private suspend fun _fetchSupportedVersion() {
|
||||
val version = githubRepository.getVersion()
|
||||
val version = githubRepository.getDiscordKtVersion()
|
||||
|
||||
version.fold(
|
||||
success = {
|
||||
|
|
|
|||
|
|
@ -11,9 +11,11 @@ import com.aliucord.manager.BuildConfig
|
|||
import com.aliucord.manager.R
|
||||
import com.aliucord.manager.domain.manager.DownloadManager
|
||||
import com.aliucord.manager.domain.manager.PreferencesManager
|
||||
import com.aliucord.manager.domain.repository.AliucordMavenRepository
|
||||
import com.aliucord.manager.domain.repository.GithubRepository
|
||||
import com.aliucord.manager.installer.util.*
|
||||
import com.aliucord.manager.network.utils.fold
|
||||
import com.aliucord.manager.network.utils.getOrThrow
|
||||
import com.aliucord.manager.ui.component.installer.InstallStatus
|
||||
import com.aliucord.manager.ui.component.installer.InstallStepData
|
||||
import com.aliucord.manager.ui.dialog.DiscordType
|
||||
|
|
@ -35,6 +37,7 @@ class InstallViewModel(
|
|||
private val downloadManager: DownloadManager,
|
||||
private val preferences: PreferencesManager,
|
||||
private val githubRepository: GithubRepository,
|
||||
private val aliucordMaven: AliucordMavenRepository,
|
||||
private val installData: InstallData
|
||||
) : ViewModel() {
|
||||
private val externalCacheDir = application.externalCacheDir!!
|
||||
|
|
@ -90,8 +93,9 @@ class InstallViewModel(
|
|||
try {
|
||||
when (installData.discordType) {
|
||||
DiscordType.REACT_NATIVE -> installReactNative()
|
||||
DiscordType.KOTLIN -> {}
|
||||
DiscordType.KOTLIN -> installKotlin()
|
||||
}
|
||||
delay(5000)
|
||||
_returnToHome.emit(true)
|
||||
} catch (t: Throwable) {
|
||||
Log.e(
|
||||
|
|
@ -134,7 +138,7 @@ class InstallViewModel(
|
|||
if (file.exists()) {
|
||||
cached = true
|
||||
} else {
|
||||
downloadManager.downloadDiscordApk(supportedVersion)
|
||||
downloadManager.downloadDiscordApk(supportedVersion, file)
|
||||
}
|
||||
|
||||
file.copyTo(
|
||||
|
|
@ -154,7 +158,8 @@ class InstallViewModel(
|
|||
cached = true
|
||||
} else downloadManager.downloadSplit(
|
||||
version = supportedVersion,
|
||||
split = "config.$libArch"
|
||||
split = "config.$libArch",
|
||||
out = file
|
||||
)
|
||||
|
||||
file.copyTo(
|
||||
|
|
@ -173,7 +178,8 @@ class InstallViewModel(
|
|||
cached = true
|
||||
} else downloadManager.downloadSplit(
|
||||
version = supportedVersion,
|
||||
split = "config.en"
|
||||
split = "config.en",
|
||||
out = file
|
||||
)
|
||||
|
||||
file.copyTo(
|
||||
|
|
@ -193,7 +199,8 @@ class InstallViewModel(
|
|||
cached = true
|
||||
} else downloadManager.downloadSplit(
|
||||
version = supportedVersion,
|
||||
split = "config.xxhdpi"
|
||||
split = "config.xxhdpi",
|
||||
out = file
|
||||
)
|
||||
|
||||
file.copyTo(
|
||||
|
|
@ -219,23 +226,21 @@ class InstallViewModel(
|
|||
val hermes = externalCacheDir.resolve("hermes-release-${latestHermesRelease.tagName}.aar").also { file ->
|
||||
if (file.exists()) return@also
|
||||
|
||||
downloadManager.download(
|
||||
url = latestHermesRelease.assets.find { it.name == "hermes-release.aar" }!!.browserDownloadUrl,
|
||||
fileName = "hermes-release-${latestHermesRelease.tagName}.aar"
|
||||
)
|
||||
latestHermesRelease.assets
|
||||
.find { it.name == "hermes-release.aar" }!!.browserDownloadUrl
|
||||
.also { downloadManager.download(it, file) }
|
||||
}
|
||||
|
||||
// Download the hermes-cppruntime-release.aar file to replace in the apk
|
||||
val cppruntime = externalCacheDir.resolve("hermes-cppruntime-release-${latestHermesRelease.tagName}.aar").also { file ->
|
||||
val cppRuntime = externalCacheDir.resolve("hermes-cppruntime-release-${latestHermesRelease.tagName}.aar").also { file ->
|
||||
if (file.exists()) return@also
|
||||
|
||||
downloadManager.download(
|
||||
url = latestHermesRelease.assets.find { it.name == "hermes-cppruntime-release.aar" }!!.browserDownloadUrl,
|
||||
fileName = "hermes-cppruntime-release-${latestHermesRelease.tagName}.aar"
|
||||
)
|
||||
latestHermesRelease.assets
|
||||
.find { it.name == "hermes-cppruntime-release.aar" }!!.browserDownloadUrl
|
||||
.also { downloadManager.download(it, file) }
|
||||
}
|
||||
|
||||
Pair(hermes, cppruntime)
|
||||
Pair(hermes, cppRuntime)
|
||||
}
|
||||
|
||||
// Download Aliucord Native lib
|
||||
|
|
@ -249,20 +254,12 @@ class InstallViewModel(
|
|||
)
|
||||
|
||||
// Download the Aliucord classes.dex file to add to the apk
|
||||
externalCacheDir.resolve("classes-${latestAliucordNativeRelease.tagName}.dex").also { file ->
|
||||
externalCacheDir.resolve("aliucord-${latestAliucordNativeRelease.tagName}.dex").also { file ->
|
||||
if (file.exists()) return@also
|
||||
|
||||
downloadManager.download(
|
||||
url = latestAliucordNativeRelease.assets.find { it.name == "classes.dex" }!!.browserDownloadUrl,
|
||||
fileName = "classes-${latestAliucordNativeRelease.tagName}.dex"
|
||||
).apply {
|
||||
copyTo(
|
||||
externalCacheDir
|
||||
.resolve("patched")
|
||||
.resolve(this.name),
|
||||
true
|
||||
)
|
||||
}
|
||||
latestAliucordNativeRelease.assets
|
||||
.find { it.name == "classes.dex" }!!.browserDownloadUrl
|
||||
.also { downloadManager.download(it, file) }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -374,6 +371,187 @@ class InstallViewModel(
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun installKotlin() {
|
||||
steps += listOfNotNull(
|
||||
InstallStep.FETCH_KT_VERSION,
|
||||
InstallStep.DL_KT_APK,
|
||||
InstallStep.DL_KOTLIN,
|
||||
InstallStep.DL_INJECTOR,
|
||||
InstallStep.DL_ALIUHOOK,
|
||||
if (preferences.replaceIcon) InstallStep.PATCH_APP_ICON else null,
|
||||
InstallStep.PATCH_MANIFEST,
|
||||
InstallStep.PATCH_DEX,
|
||||
InstallStep.PATCH_LIBS,
|
||||
InstallStep.SIGN_APK,
|
||||
InstallStep.INSTALL_APK,
|
||||
).map {
|
||||
it to InstallStepData(it.nameResId, InstallStatus.QUEUED)
|
||||
}
|
||||
|
||||
val dataJson = step(InstallStep.FETCH_KT_VERSION) {
|
||||
githubRepository.getDiscordKtVersion().getOrThrow()
|
||||
}
|
||||
|
||||
val arch = Build.SUPPORTED_ABIS.first()
|
||||
val cacheDir = externalCacheDir
|
||||
val discordCacheDir = externalCacheDir.resolve(dataJson.versionCode)
|
||||
val patchedDir = discordCacheDir.resolve("patched").also { it.deleteRecursively() }
|
||||
|
||||
// Download base.apk
|
||||
val baseApkFile = step(InstallStep.DL_KT_APK) {
|
||||
discordCacheDir.resolve("base.apk").let { file ->
|
||||
if (file.exists()) {
|
||||
cached = true
|
||||
} else {
|
||||
downloadManager.downloadDiscordApk(dataJson.versionCode, file)
|
||||
}
|
||||
|
||||
file.copyTo(
|
||||
patchedDir.resolve(file.name),
|
||||
true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val kotlinFile = step(InstallStep.DL_KOTLIN) {
|
||||
cacheDir.resolve("kotlin.dex").also { file ->
|
||||
if (file.exists()) {
|
||||
cached = true
|
||||
} else {
|
||||
downloadManager.downloadKotlinDex(file)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Download the injector dex
|
||||
val injectorFile = step(InstallStep.DL_INJECTOR) {
|
||||
cacheDir.resolve("injector-${dataJson.aliucordHash}.dex").also { file ->
|
||||
if (file.exists()) {
|
||||
cached = true
|
||||
} else {
|
||||
downloadManager.downloadKtInjector(file)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Download Aliuhook aar
|
||||
val aliuhookAarFile = step(InstallStep.DL_ALIUHOOK) {
|
||||
// Fetch aliuhook version
|
||||
val aliuhookVersion = aliucordMaven.getAliuhookVersion().getOrThrow()
|
||||
|
||||
// Download aliuhook aar
|
||||
cacheDir.resolve("aliuhook-${aliuhookVersion}.aar").also { file ->
|
||||
if (file.exists()) {
|
||||
cached = true
|
||||
} else {
|
||||
downloadManager.downloadAliuhook(aliuhookVersion, file)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Replace app icons
|
||||
if (preferences.replaceIcon) {
|
||||
step(InstallStep.PATCH_APP_ICON) {
|
||||
ZipWriter(baseApkFile, true).use { baseApk ->
|
||||
val foregroundIcon = application.assets.open("icons/ic_logo_foreground.png")
|
||||
.use { it.readBytes() }
|
||||
val squareIcon = application.assets.open("icons/ic_logo_square.png")
|
||||
.use { it.readBytes() }
|
||||
|
||||
val replacements = mapOf(
|
||||
arrayOf("MbV.png", "kbF.png", "_eu.png", "EtS.png") to foregroundIcon,
|
||||
arrayOf("_h_.png", "9MB.png", "Dy7.png", "kC0.png", "oEH.png", "RG0.png", "ud_.png", "W_3.png") to squareIcon
|
||||
)
|
||||
|
||||
for ((files, replacement) in replacements) {
|
||||
for (file in files) {
|
||||
val path = "res/$file"
|
||||
baseApk.deleteEntry(path)
|
||||
baseApk.writeEntry(path, replacement)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Patch manifests
|
||||
step(InstallStep.PATCH_MANIFEST) {
|
||||
val manifest = ZipReader(baseApkFile)
|
||||
.use { zip -> zip.openEntry("AndroidManifest.xml")?.read() }
|
||||
?: throw IllegalStateException("No manifest in base apk")
|
||||
|
||||
ZipWriter(baseApkFile, true).use { zip ->
|
||||
val patchedManifestBytes = ManifestPatcher.patchManifest(
|
||||
manifestBytes = manifest,
|
||||
packageName = preferences.packageName,
|
||||
appName = preferences.appName,
|
||||
debuggable = preferences.debuggable,
|
||||
)
|
||||
|
||||
zip.deleteEntry("AndroidManifest.xml")
|
||||
zip.writeEntry("AndroidManifest.xml", patchedManifestBytes)
|
||||
}
|
||||
}
|
||||
|
||||
// Re-order dex files
|
||||
val dexCount = step(InstallStep.PATCH_DEX) {
|
||||
val (dexCount, firstDexBytes) = ZipReader(baseApkFile).use { zip ->
|
||||
Pair(
|
||||
// Find the amount of .dex files in apk
|
||||
zip.entryNames.count { it.endsWith(".dex") },
|
||||
|
||||
// Get the first dex
|
||||
zip.openEntry("classes.dex")?.read()
|
||||
?: throw IllegalStateException("No classes.dex in base apk")
|
||||
)
|
||||
}
|
||||
|
||||
ZipWriter(baseApkFile, true).use { zip ->
|
||||
// Move copied dex to end of dex list
|
||||
zip.deleteEntry("classes.dex")
|
||||
zip.writeEntry("classes${dexCount + 1}.dex", firstDexBytes)
|
||||
|
||||
// Add Kotlin & Aliucord's dex
|
||||
zip.writeEntry("classes.dex", injectorFile.readBytes())
|
||||
zip.writeEntry("classes${dexCount + 2}.dex", kotlinFile.readBytes())
|
||||
}
|
||||
|
||||
dexCount
|
||||
}
|
||||
|
||||
// Replace libs
|
||||
step(InstallStep.PATCH_LIBS) {
|
||||
ZipWriter(baseApkFile, true).use { baseApk ->
|
||||
ZipReader(aliuhookAarFile).use { aliuhookAar ->
|
||||
for (libFile in arrayOf("libaliuhook.so", "libc++_shared.so", "liblsplant.so")) {
|
||||
val bytes = aliuhookAar.openEntry("jni/$arch/$libFile")?.read()
|
||||
?: throw IllegalStateException("Failed to read $libFile from aliuhook aar")
|
||||
|
||||
baseApk.writeEntry("lib/$arch/$libFile", bytes)
|
||||
}
|
||||
|
||||
// Add Aliuhook's dex file
|
||||
val aliuhookDex = aliuhookAar.openEntry("classes.dex")?.read()
|
||||
?: throw IllegalStateException("No classes.dex in aliuhook aar")
|
||||
|
||||
baseApk.writeEntry("classes${dexCount + 3}.dex", aliuhookDex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
step(InstallStep.SIGN_APK) {
|
||||
Signer.signApk(baseApkFile)
|
||||
}
|
||||
|
||||
step(InstallStep.INSTALL_APK) {
|
||||
application.packageManager.packageInstaller
|
||||
.installApks(application, baseApkFile)
|
||||
}
|
||||
|
||||
patchedDir.deleteRecursively()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalTime::class)
|
||||
private inline fun <T> step(step: InstallStep, block: InstallStepData.() -> T): T {
|
||||
steps[step] = steps[step]!!.copy(status = InstallStatus.ONGOING)
|
||||
|
|
@ -415,12 +593,14 @@ class InstallViewModel(
|
|||
INSTALLING(R.string.install_group_install)
|
||||
}
|
||||
|
||||
// Order matters, define it in the same order as it is patched
|
||||
enum class InstallStep(
|
||||
val group: InstallStepGroup,
|
||||
|
||||
@StringRes
|
||||
val nameResId: Int
|
||||
) {
|
||||
// React Native
|
||||
DL_BASE_APK(InstallStepGroup.APK_DL, R.string.install_step_dl_apk_base),
|
||||
DL_LIBS_APK(InstallStepGroup.APK_DL, R.string.install_step_dl_apk_lib),
|
||||
DL_LANG_APK(InstallStepGroup.APK_DL, R.string.install_step_dl_apk_locale),
|
||||
|
|
@ -429,6 +609,14 @@ class InstallViewModel(
|
|||
DL_HERMES(InstallStepGroup.LIB_DL, R.string.install_step_dl_lib_hermes),
|
||||
DL_ALIUNATIVE(InstallStepGroup.LIB_DL, R.string.install_step_dl_lib_aliunative),
|
||||
|
||||
// Kotlin
|
||||
FETCH_KT_VERSION(InstallStepGroup.APK_DL, R.string.install_step_fetch_kt_version),
|
||||
DL_KT_APK(InstallStepGroup.APK_DL, R.string.install_step_dl_kt_apk),
|
||||
DL_KOTLIN(InstallStepGroup.LIB_DL, R.string.install_step_dl_kotlin),
|
||||
DL_INJECTOR(InstallStepGroup.LIB_DL, R.string.install_step_dl_injector),
|
||||
DL_ALIUHOOK(InstallStepGroup.LIB_DL, R.string.install_step_dl_aliuhook),
|
||||
|
||||
// Common
|
||||
PATCH_APP_ICON(InstallStepGroup.PATCHING, R.string.install_step_patch_icons),
|
||||
PATCH_MANIFEST(InstallStepGroup.PATCHING, R.string.install_step_patch_manifests),
|
||||
PATCH_DEX(InstallStepGroup.PATCHING, R.string.install_step_patch_dex),
|
||||
|
|
|
|||
76
app/src/main/res/values-ru/strings.xml
Normal file
76
app/src/main/res/values-ru/strings.xml
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Менеджер Алиюкорда</string>
|
||||
<string name="app_description">A modification for the Discord Android App</string>
|
||||
<string name="aliucord">Алиюкорд</string>
|
||||
<string name="github">GitHub</string>
|
||||
<string name="support_server">Support Server</string>
|
||||
<string name="commits">Commits</string>
|
||||
<string name="team">Team</string>
|
||||
<string name="website">Website</string>
|
||||
<string name="installer">Installer</string>
|
||||
<string name="action_retry">Повторить</string>
|
||||
<string name="action_apply">Применить</string>
|
||||
<string name="action_confirm">Подтвердить</string>
|
||||
<string name="action_dismiss">Скрыть</string>
|
||||
<string name="action_install">Установить</string>
|
||||
<string name="action_reinstall">Переустановить</string>
|
||||
<string name="action_update">Обновить</string>
|
||||
<string name="action_clear">Стереть</string>
|
||||
<string name="action_launch">Запустить</string>
|
||||
<string name="action_uninstall">Удалить</string>
|
||||
<string name="action_close">Закрыть</string>
|
||||
<string name="action_open_settings">Открыть настройки</string>
|
||||
<string name="action_search">Поиск</string>
|
||||
<string name="action_loading_trailing">Loading…</string>
|
||||
<string name="permissions_grant_title">Grant Permissions</string>
|
||||
<string name="permissions_grant_body">In order for Aliucord Manager to work properly, you need to grant file permissions. Since shared data in stored in ~/Aliucord, permissions are required in order for manager to function.</string>
|
||||
<string name="install_success">Successfully installed Aliucord</string>
|
||||
<string name="install_aborted">Aborted Aliucord installation</string>
|
||||
<string name="install_failed">Установка завершилась с ошибкой: %o</string>
|
||||
<string name="launch_aliucord_fail">Failed to launch Aliucord</string>
|
||||
<string name="settings_theme">Тема</string>
|
||||
<string name="settings_advanced">Advanced</string>
|
||||
<string name="settings_appearance">Appearance</string>
|
||||
<string name="settings_developer_options">Developer options</string>
|
||||
<string name="setting_clear_cache">Clear cache</string>
|
||||
<string name="setting_dex_location">Dex location</string>
|
||||
<string name="setting_use_dex_from_storage">Use Injector.dex from storage</string>
|
||||
<string name="setting_replace_icon">Replace app icon</string>
|
||||
<string name="setting_replace_icon_desc">Uses the Aliucord variant as opposed to Discord\'s own icon</string>
|
||||
<string name="setting_debuggable">Debuggable</string>
|
||||
<string name="setting_debuggable_desc">Enable debuggable flag</string>
|
||||
<string name="setting_theme_desc">Manager appearance</string>
|
||||
<string name="setting_app_name">App Name</string>
|
||||
<string name="setting_package_name">Package Name</string>
|
||||
<string name="setting_target_version">Версия загрузки</string>
|
||||
<string name="setting_dynamic_color">Dynamic color</string>
|
||||
<string name="theme_system">Системная</string>
|
||||
<string name="theme_dark">Тёмная</string>
|
||||
<string name="theme_light">Светлая</string>
|
||||
<string name="plugins_title">Плагины</string>
|
||||
<string name="plugins_view_changelog">Открыть список изменений для %1$s</string>
|
||||
<string name="plugins_delete_plugin_body">Are you sure you want to uninstall this plugin?</string>
|
||||
<string name="plugins_search">Поиск плагинов</string>
|
||||
<string name="plugins_none_installed">No plugins installed!</string>
|
||||
<string name="plugins_changelog_media">Changelog Media</string>
|
||||
<string name="plugins_plugin_title">§plugin§%1$s§ v%2$s</string>
|
||||
<string name="navigation_back">Назад</string>
|
||||
<string name="navigation_home">Главная</string>
|
||||
<string name="navigation_about">About</string>
|
||||
<string name="navigation_settings">Настройки</string>
|
||||
<string name="contributors">Contributors</string>
|
||||
<string name="contributors_contributions">%o contributions</string>
|
||||
<string name="contributors_contributor">Contributor</string>
|
||||
<string name="contributors_commit_title">§name§%1$s§ §middle§создал§ §commit§%2$s§</string>
|
||||
<string name="selector_download_apk">Download APK</string>
|
||||
<string name="selector_select_apk">Select APK</string>
|
||||
<string name="selector_download_method">Download Method</string>
|
||||
<string name="network_load_fail">Ошибка загрузки</string>
|
||||
<string name="version_supported">Текущая версия:</string>
|
||||
<string name="version_installed">Установленная версия:</string>
|
||||
<string name="version_stable">Stable</string>
|
||||
<string name="version_beta">Beta</string>
|
||||
<string name="version_alpha">Alpha</string>
|
||||
<string name="version_load_fail">Failed to retrieve version</string>
|
||||
</resources>
|
||||
|
|
@ -13,7 +13,7 @@
|
|||
<string name="action_retry">Retry</string>
|
||||
<string name="action_apply">Apply</string>
|
||||
<string name="action_confirm">Confirm</string>
|
||||
<string name="action_dismiss">Dismiss</string>
|
||||
<string name="action_cancel">Cancel</string>
|
||||
<string name="action_install">Install</string>
|
||||
<string name="action_reinstall">Reinstall</string>
|
||||
<string name="action_update">Update</string>
|
||||
|
|
@ -74,8 +74,10 @@
|
|||
<string name="contributors_commit_title">§name§%1$s§ §middle§authored§ §commit§%2$s§</string>
|
||||
|
||||
<string name="selector_download_apk">Download APK</string>
|
||||
<string name="selector_select_apk">Select APK</string>
|
||||
<string name="selector_download_method">Download Method</string>
|
||||
<string name="selector_discord_type">Discord Type</string>
|
||||
<string name="selector_discord_type_body">Aliucord supports both the old (Kotlin) and new (React Native) versions of Discord. Which type would you like to install? Note that these versions have different plugins!</string>
|
||||
<string name="selector_discord_type_kotlin">New (Recommended)</string>
|
||||
<string name="selector_discord_type_old">Old</string>
|
||||
|
||||
<string name="network_load_fail">Failed to load</string>
|
||||
|
||||
|
|
@ -113,6 +115,11 @@
|
|||
<string name="install_step_dl_apk_resource">Downloading resource apk</string>
|
||||
<string name="install_step_dl_lib_hermes">Downloading hermes library</string>
|
||||
<string name="install_step_dl_lib_aliunative">Downloading AliucordNative library</string>
|
||||
<string name="install_step_fetch_kt_version">Fetching target Discord version</string>
|
||||
<string name="install_step_dl_kt_apk">Downloading Discord APK</string>
|
||||
<string name="install_step_dl_kotlin">Downloading Kotlin library</string>
|
||||
<string name="install_step_dl_injector">Downloading injector</string>
|
||||
<string name="install_step_dl_aliuhook">Downloading Aliuhook library</string>
|
||||
|
||||
<string name="status_success">Success</string>
|
||||
<string name="status_ongoing">Ongoing</string>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue