feat(wintry): Allow inserting custom version code, fetch latest metadata for latest updates, embed lspatch native libraries
This commit is contained in:
parent
5ef7946a9c
commit
9d2458c523
37 changed files with 514 additions and 96 deletions
BIN
app/libs/lspatch.aar
Normal file
BIN
app/libs/lspatch.aar
Normal file
Binary file not shown.
BIN
app/src/main/assets/lspatch/so/arm64-v8a/liblspatch.so
Normal file
BIN
app/src/main/assets/lspatch/so/arm64-v8a/liblspatch.so
Normal file
Binary file not shown.
BIN
app/src/main/assets/lspatch/so/armeabi-v7a/liblspatch.so
Normal file
BIN
app/src/main/assets/lspatch/so/armeabi-v7a/liblspatch.so
Normal file
Binary file not shown.
BIN
app/src/main/assets/lspatch/so/x86/liblspatch.so
Normal file
BIN
app/src/main/assets/lspatch/so/x86/liblspatch.so
Normal file
Binary file not shown.
BIN
app/src/main/assets/lspatch/so/x86_64/liblspatch.so
Normal file
BIN
app/src/main/assets/lspatch/so/x86_64/liblspatch.so
Normal file
Binary file not shown.
|
|
@ -48,6 +48,8 @@ class ManagerApplication : Application() {
|
|||
singleOf(::HttpService)
|
||||
singleOf(::AliucordGithubService)
|
||||
singleOf(::AliucordMavenService)
|
||||
singleOf(::RNATrackerService)
|
||||
singleOf(::WintryGithubService)
|
||||
})
|
||||
|
||||
// UI Models
|
||||
|
|
|
|||
|
|
@ -116,6 +116,10 @@ class PathManager(
|
|||
fun cachedKotlinDex() = patchingDownloadDir
|
||||
.resolve("kotlin.dex")
|
||||
|
||||
fun cachedWintryXposed(version: SemVer) = patchingDownloadDir
|
||||
.resolve("wt-xposed")
|
||||
.resolve("$version.apk")
|
||||
|
||||
/**
|
||||
* The temporary working directory of a currently executing patching process.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ data class GithubRelease(
|
|||
val assets: List<GithubReleaseAssets>,
|
||||
@SerialName("tag_name")
|
||||
val tagName: String,
|
||||
@SerialName("name")
|
||||
val name: String,
|
||||
) {
|
||||
@Serializable
|
||||
data class GithubReleaseAssets(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
package com.aliucord.manager.network.models
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class RNATrackerIndex(
|
||||
val latest: Versions,
|
||||
) {
|
||||
@Serializable
|
||||
data class Versions(
|
||||
val alpha: Int,
|
||||
val beta: Int,
|
||||
val stable: Int,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.aliucord.manager.network.services
|
||||
|
||||
import com.aliucord.manager.network.models.RNATrackerIndex
|
||||
import io.ktor.client.request.url
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class RNATrackerService(
|
||||
private val httpService: HttpService,
|
||||
) {
|
||||
suspend fun getLatestDiscordVersions() = withContext(Dispatchers.IO) {
|
||||
httpService.request<RNATrackerIndex> {
|
||||
url("${BASE_URL}/tracker/index")
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val BASE_URL = "https://tracker.vendetta.rocks"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package com.aliucord.manager.network.services
|
||||
|
||||
import com.aliucord.manager.network.models.GithubRelease
|
||||
import com.aliucord.manager.network.utils.ApiResponse
|
||||
import io.ktor.client.request.header
|
||||
import io.ktor.client.request.url
|
||||
import io.ktor.http.HttpHeaders
|
||||
|
||||
class WintryGithubService(
|
||||
private val http: HttpService,
|
||||
) {
|
||||
/**
|
||||
* Fetches all the Manager releases with a 60s local cache.
|
||||
*/
|
||||
suspend fun getManagerReleases(): ApiResponse<List<GithubRelease>> {
|
||||
return http.request {
|
||||
url("https://api.github.com/repos/$ORG/$MANAGER_REPO/releases")
|
||||
header(HttpHeaders.CacheControl, "public, max-age=60, s-maxage=60")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the latest Xposed release with a 60s local cache.
|
||||
*/
|
||||
suspend fun getLatestXposedRelease(): ApiResponse<GithubRelease> {
|
||||
return http.request {
|
||||
url("https://api.github.com/repos/$ORG/$XPOSED_REPO/releases/latest")
|
||||
header(HttpHeaders.CacheControl, "public, max-age=60, s-maxage=60")
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ORG = "wtcord"
|
||||
const val MANAGER_REPO = "wt-manager"
|
||||
const val XPOSED_REPO = "wt-xposed"
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@ import com.aliucord.manager.ui.screens.patchopts.PatchOptions
|
|||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Data stored inside patched APKs as "aliucord.json" in order to preserve install-time information about Aliucord and the Manager.
|
||||
* Data stored inside patched APKs as ~~"aliucord.json"~~ "wintry.json" in order to preserve install-time information about Aliucord and the Manager.
|
||||
*/
|
||||
@Serializable
|
||||
data class InstallMetadata(
|
||||
|
|
@ -38,4 +38,14 @@ data class InstallMetadata(
|
|||
* Version of the smali patches that were applied onto the APK.
|
||||
*/
|
||||
val patchesVersion: SemVer?,
|
||||
|
||||
/**
|
||||
* Version of LSPatch used to inject Xposed module into the APK.
|
||||
*/
|
||||
val lspatchVersion: Int?,
|
||||
|
||||
/**
|
||||
* Version of WintryXposed embedded into the APK.
|
||||
*/
|
||||
val wintryXposedVersion: SemVer?,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -3,16 +3,21 @@ package com.aliucord.manager.patcher
|
|||
import com.aliucord.manager.patcher.steps.download.*
|
||||
import com.aliucord.manager.patcher.steps.install.*
|
||||
import com.aliucord.manager.patcher.steps.patch.*
|
||||
import com.aliucord.manager.patcher.steps.prepare.*
|
||||
import com.aliucord.manager.ui.screens.patchopts.PatchOptions
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
class WintryPatchRunner(options: PatchOptions) : StepRunner() {
|
||||
override val steps = persistentListOf(
|
||||
FetchDiscordRNAStep(options),
|
||||
DowngradeCheckStep(options),
|
||||
RestoreDownloadsStep(),
|
||||
|
||||
DownloadDiscordRNABaseStep(),
|
||||
DownloadDiscordRNALangStep(),
|
||||
DownloadDiscordRNAResourcesStep(),
|
||||
DownloadDiscordRNALibStep(),
|
||||
DownloadWintryXposed(),
|
||||
DownloadWintryXposedStep(),
|
||||
CopyDependenciesStep(),
|
||||
|
||||
PatchIconsStep(options),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
package com.aliucord.manager.patcher.steps.base
|
||||
|
||||
import com.aliucord.manager.patcher.StepRunner
|
||||
import com.aliucord.manager.patcher.steps.prepare.FetchDiscordRNAStep
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
abstract class DownloadDiscordRNAStep : DownloadStep() {
|
||||
protected var version by Delegates.notNull<Int>()
|
||||
private set
|
||||
|
||||
override suspend fun execute(container: StepRunner) {
|
||||
version = container.getStep<FetchDiscordRNAStep>().targetVersion
|
||||
super.execute(container)
|
||||
}
|
||||
}
|
||||
|
|
@ -22,9 +22,6 @@ abstract class DownloadStep : Step(), KoinComponent {
|
|||
private val context: Context by inject()
|
||||
private val downloader: KtorDownloadManager by inject()
|
||||
|
||||
// Temporary
|
||||
protected val version: Int = 289210
|
||||
|
||||
/**
|
||||
* The remote url to download
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ class CopyDependenciesStep : Step(), KoinComponent {
|
|||
override val localizedName = R.string.patch_step_copy_deps
|
||||
|
||||
override suspend fun execute(container: StepRunner) {
|
||||
val srcApk = container.getStep<DownloadDiscordStep>().targetFile
|
||||
val srcApk = container.getStepOrNull<DownloadDiscordRNABaseStep>()?.targetFile ?: container.getStepOrNull<DownloadDiscordStep>()!!.targetFile
|
||||
val langApk = container.getStepOrNull<DownloadDiscordRNALangStep>()?.targetFile
|
||||
val libApk = container.getStepOrNull<DownloadDiscordRNALibStep>()?.targetFile
|
||||
val resApk = container.getStepOrNull<DownloadDiscordRNAResourcesStep>()?.targetFile
|
||||
|
|
|
|||
|
|
@ -1,9 +1,16 @@
|
|||
package com.aliucord.manager.patcher.steps.download
|
||||
|
||||
import com.aliucord.manager.R
|
||||
import com.aliucord.manager.manager.PathManager
|
||||
import com.aliucord.manager.network.services.RNATrackerService.Companion.BASE_URL
|
||||
import com.aliucord.manager.patcher.steps.base.DownloadDiscordRNAStep
|
||||
import org.koin.core.component.inject
|
||||
|
||||
class DownloadDiscordRNABaseStep : DownloadDiscordStep() {
|
||||
override val targetUrl = "https://tracker.vendetta.rocks/tracker/download/$version/base"
|
||||
override val targetFile = paths.discordApkVersionCache(version).resolve("base.apk")
|
||||
class DownloadDiscordRNABaseStep : DownloadDiscordRNAStep() {
|
||||
private val paths: PathManager by inject()
|
||||
|
||||
override val targetUrl get() = "$BASE_URL/tracker/download/$version/base"
|
||||
override val targetFile get() = paths.discordApkVersionCache(version).resolve("base.apk")
|
||||
override val localizedName = R.string.downloading_base_apk
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,14 +2,15 @@ package com.aliucord.manager.patcher.steps.download
|
|||
|
||||
import com.aliucord.manager.R
|
||||
import com.aliucord.manager.manager.PathManager
|
||||
import com.aliucord.manager.patcher.steps.base.DownloadStep
|
||||
import org.koin.core.component.KoinComponent
|
||||
import com.aliucord.manager.network.services.RNATrackerService.Companion.BASE_URL
|
||||
import com.aliucord.manager.patcher.steps.base.DownloadDiscordRNAStep
|
||||
import org.koin.core.component.inject
|
||||
|
||||
class DownloadDiscordRNALangStep : DownloadStep(), KoinComponent {
|
||||
class DownloadDiscordRNALangStep : DownloadDiscordRNAStep() {
|
||||
private val paths: PathManager by inject()
|
||||
|
||||
override val targetUrl = "https://tracker.vendetta.rocks/tracker/download/$version/config.en"
|
||||
override val targetFile = paths.discordApkVersionCache(version).resolve("config.en.apk")
|
||||
override val targetUrl get() = "$BASE_URL/tracker/download/$version/config.en"
|
||||
override val targetFile get() = paths.discordApkVersionCache(version).resolve("config.en.apk")
|
||||
|
||||
override val localizedName = R.string.download_libs_apk
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,15 +3,15 @@ package com.aliucord.manager.patcher.steps.download
|
|||
import android.os.Build
|
||||
import com.aliucord.manager.R
|
||||
import com.aliucord.manager.manager.PathManager
|
||||
import com.aliucord.manager.patcher.steps.base.DownloadStep
|
||||
import org.koin.core.component.KoinComponent
|
||||
import com.aliucord.manager.network.services.RNATrackerService.Companion.BASE_URL
|
||||
import com.aliucord.manager.patcher.steps.base.DownloadDiscordRNAStep
|
||||
import org.koin.core.component.inject
|
||||
|
||||
class DownloadDiscordRNALibStep : DownloadStep(), KoinComponent {
|
||||
class DownloadDiscordRNALibStep : DownloadDiscordRNAStep() {
|
||||
private val paths: PathManager by inject()
|
||||
private val arch = Build.SUPPORTED_ABIS.first().replace("-v", "_v")
|
||||
|
||||
override val targetUrl = "https://tracker.vendetta.rocks/tracker/download/$version/config.$arch"
|
||||
override val targetFile = paths.discordApkVersionCache(version).resolve("config.$arch.apk")
|
||||
override val targetUrl get() = "$BASE_URL/tracker/download/$version/config.$arch"
|
||||
override val targetFile get() = paths.discordApkVersionCache(version).resolve("config.$arch.apk")
|
||||
override val localizedName = R.string.downloading_libs_apk
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,14 +2,14 @@ package com.aliucord.manager.patcher.steps.download
|
|||
|
||||
import com.aliucord.manager.R
|
||||
import com.aliucord.manager.manager.PathManager
|
||||
import com.aliucord.manager.patcher.steps.base.DownloadStep
|
||||
import org.koin.core.component.KoinComponent
|
||||
import com.aliucord.manager.network.services.RNATrackerService.Companion.BASE_URL
|
||||
import com.aliucord.manager.patcher.steps.base.DownloadDiscordRNAStep
|
||||
import org.koin.core.component.inject
|
||||
|
||||
class DownloadDiscordRNAResourcesStep : DownloadStep(), KoinComponent {
|
||||
class DownloadDiscordRNAResourcesStep : DownloadDiscordRNAStep() {
|
||||
private val paths: PathManager by inject()
|
||||
|
||||
override val targetUrl = "https://tracker.vendetta.rocks/tracker/download/$version/config.xxhdpi"
|
||||
override val targetFile = paths.discordApkVersionCache(version).resolve("config.xxhdpi.apk")
|
||||
override val targetUrl get() = "$BASE_URL/tracker/download/$version/config.xxhdpi"
|
||||
override val targetFile get() = paths.discordApkVersionCache(version).resolve("config.xxhdpi.apk")
|
||||
override val localizedName = R.string.downloading_resources_apk
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ open class DownloadDiscordStep : DownloadStep(), KoinComponent {
|
|||
protected val paths: PathManager by inject()
|
||||
|
||||
override val localizedName = R.string.patch_step_dl_kt_apk
|
||||
|
||||
override val targetUrl = getDiscordApkUrl(DISCORD_KT_VERSION)
|
||||
override val targetFile = paths.discordApkVersionCache(DISCORD_KT_VERSION)
|
||||
.resolve("discord.apk")
|
||||
|
|
|
|||
|
|
@ -1,14 +0,0 @@
|
|||
package com.aliucord.manager.patcher.steps.download
|
||||
|
||||
import com.aliucord.manager.R
|
||||
import com.aliucord.manager.manager.PathManager
|
||||
import com.aliucord.manager.patcher.steps.base.DownloadStep
|
||||
import org.koin.core.component.inject
|
||||
|
||||
class DownloadWintryXposed : DownloadStep() {
|
||||
private val paths: PathManager by inject()
|
||||
|
||||
override val localizedName = R.string.downloading_wintryxposed
|
||||
override val targetFile = paths.patchingDownloadDir.resolve("xposed.apk")
|
||||
override val targetUrl = "https://github.com/wtcord/wt-xposed/releases/download/1002/app-release.apk"
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package com.aliucord.manager.patcher.steps.download
|
||||
|
||||
import com.aliucord.manager.R
|
||||
import com.aliucord.manager.manager.PathManager
|
||||
import com.aliucord.manager.network.services.WintryGithubService
|
||||
import com.aliucord.manager.network.utils.SemVer
|
||||
import com.aliucord.manager.network.utils.getOrThrow
|
||||
import com.aliucord.manager.patcher.StepRunner
|
||||
import com.aliucord.manager.patcher.steps.base.DownloadStep
|
||||
import org.koin.core.component.inject
|
||||
|
||||
class DownloadWintryXposedStep : DownloadStep() {
|
||||
private val paths: PathManager by inject()
|
||||
private val wintryGithubService: WintryGithubService by inject()
|
||||
|
||||
override val localizedName = R.string.downloading_wintryxposed
|
||||
override val targetFile get() = paths.cachedWintryXposed(targetVersion)
|
||||
override lateinit var targetUrl: String
|
||||
|
||||
lateinit var targetVersion: SemVer
|
||||
private set
|
||||
|
||||
override suspend fun execute(container: StepRunner) {
|
||||
val latestRelease = wintryGithubService.getLatestXposedRelease().getOrThrow()
|
||||
container.log("Latest WintryXposed release is ${latestRelease.name}")
|
||||
|
||||
targetVersion = SemVer.parse(latestRelease.name)
|
||||
targetUrl = latestRelease.assets.first { it.name == "app-release.apk" }.browserDownloadUrl
|
||||
super.execute(container)
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@ import com.aliucord.manager.patcher.StepRunner
|
|||
import com.aliucord.manager.patcher.steps.StepGroup
|
||||
import com.aliucord.manager.patcher.steps.base.Step
|
||||
import com.aliucord.manager.patcher.steps.download.CopyDependenciesStep
|
||||
import com.aliucord.manager.patcher.steps.download.DownloadWintryXposed
|
||||
import com.aliucord.manager.patcher.steps.download.DownloadWintryXposedStep
|
||||
import com.aliucord.manager.patcher.util.Signer
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
|
@ -58,7 +58,7 @@ class InjectWintryXposedStep : Step() {
|
|||
|
||||
override suspend fun execute(container: StepRunner) {
|
||||
val apks = container.getStep<CopyDependenciesStep>().patchedApks
|
||||
val xposed = container.getStep<DownloadWintryXposed>().targetFile
|
||||
val xposed = container.getStep<DownloadWintryXposedStep>().targetFile
|
||||
|
||||
container.log("Adding WintryXposed module with LSPatch")
|
||||
container.log("WintryXposed path = ${xposed.absolutePath}")
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import com.github.diamondminer88.zip.ZipWriter
|
|||
import kotlinx.serialization.json.Json
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import org.lsposed.lspatch.share.LSPConfig
|
||||
|
||||
/**
|
||||
* Store the install options and additional data inside the APK for future use,
|
||||
|
|
@ -30,6 +31,7 @@ class SaveMetadataStep(private val options: PatchOptions) : Step(), KoinComponen
|
|||
val aliuhook = container.getStepOrNull<DownloadAliuhookStep>()
|
||||
val injector = container.getStepOrNull<DownloadInjectorStep>()
|
||||
val patches = container.getStepOrNull<DownloadPatchesStep>()
|
||||
val wintryXposed = container.getStepOrNull<DownloadWintryXposedStep>()
|
||||
|
||||
val metadata = InstallMetadata(
|
||||
options = options,
|
||||
|
|
@ -38,11 +40,13 @@ class SaveMetadataStep(private val options: PatchOptions) : Step(), KoinComponen
|
|||
aliuhookVersion = aliuhook?.targetVersion,
|
||||
injectorVersion = injector?.targetVersion,
|
||||
patchesVersion = patches?.targetVersion,
|
||||
lspatchVersion = LSPConfig.instance.VERSION_CODE,
|
||||
wintryXposedVersion = wintryXposed?.targetVersion,
|
||||
)
|
||||
|
||||
container.log("Writing serialized install metadata to APK")
|
||||
ZipWriter(apk, /* append = */ true).use {
|
||||
it.writeEntry("aliucord.json", json.encodeToString<InstallMetadata>(metadata))
|
||||
it.writeEntry("wintry.json", json.encodeToString<InstallMetadata>(metadata))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,9 +38,15 @@ class DowngradeCheckStep(private val options: PatchOptions) : Step(), KoinCompon
|
|||
}
|
||||
container.log("Version of installed Discord app: $currentVersion")
|
||||
|
||||
val targetVersion = container
|
||||
.getStep<FetchInfoStep>()
|
||||
.data.discordVersionCode
|
||||
var targetVersion: Int
|
||||
|
||||
val fetchInfo = container.getStepOrNull<FetchInfoStep>()
|
||||
if (fetchInfo != null) {
|
||||
targetVersion = fetchInfo.data.discordVersionCode
|
||||
} else {
|
||||
val rnaFetchInfo = container.getStep<FetchDiscordRNAStep>()
|
||||
targetVersion = rnaFetchInfo.targetVersion
|
||||
}
|
||||
|
||||
container.log("Target discord version: $targetVersion")
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
package com.aliucord.manager.patcher.steps.prepare
|
||||
|
||||
import com.aliucord.manager.R
|
||||
import com.aliucord.manager.network.services.RNATrackerService
|
||||
import com.aliucord.manager.network.utils.getOrThrow
|
||||
import com.aliucord.manager.patcher.StepRunner
|
||||
import com.aliucord.manager.patcher.steps.StepGroup
|
||||
import com.aliucord.manager.patcher.steps.base.Step
|
||||
import com.aliucord.manager.ui.screens.patchopts.PatchOptions
|
||||
import com.aliucord.manager.ui.screens.patchopts.VersionPreference
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
class FetchDiscordRNAStep(val options: PatchOptions) : Step(), KoinComponent {
|
||||
private val rnaTrackerService: RNATrackerService by inject()
|
||||
|
||||
override val group: StepGroup = StepGroup.Prepare
|
||||
override val localizedName: Int = R.string.fetching_latest_discord_info
|
||||
|
||||
var targetVersion by Delegates.notNull<Int>()
|
||||
private set
|
||||
|
||||
override suspend fun execute(container: StepRunner) {
|
||||
if (options.versionPreference == VersionPreference.Custom) {
|
||||
container.log("Custom version selected, skipping fetch.")
|
||||
targetVersion = options.customVersionCode.toInt()
|
||||
return
|
||||
}
|
||||
|
||||
val latestVal = rnaTrackerService.getLatestDiscordVersions().getOrThrow()
|
||||
container.log("Fetched latest discord info: $latestVal")
|
||||
|
||||
targetVersion = when (options.versionPreference) {
|
||||
VersionPreference.Stable -> latestVal.latest.stable
|
||||
VersionPreference.Beta -> latestVal.latest.beta
|
||||
VersionPreference.Alpha -> latestVal.latest.alpha
|
||||
else -> throw IllegalArgumentException("Unsupported version preference: ${options.versionPreference}")
|
||||
}
|
||||
|
||||
container.log("Selected Discord version: $targetVersion")
|
||||
}
|
||||
}
|
||||
|
|
@ -35,7 +35,7 @@ object ManifestPatcher {
|
|||
vmSafeMode: Boolean? = true,
|
||||
useEmbeddedDex: Boolean? = true,
|
||||
extractNativeLibs: Boolean? = false,
|
||||
addAliucordMetadata: Boolean? = true,
|
||||
addManagerMetadata: Boolean? = true,
|
||||
modifyPermissions: Boolean? = true,
|
||||
modifyProviders: Boolean? = true,
|
||||
modifyActivities: Boolean? = true,
|
||||
|
|
@ -128,7 +128,7 @@ object ManifestPatcher {
|
|||
private var shouldAddLegacyStorage = requestLegacyExternalStorage ?: false
|
||||
private var shouldAddUseEmbeddedDex = useEmbeddedDex ?: false
|
||||
private var shouldAddExtractNativeLibs = extractNativeLibs ?: false
|
||||
private var shouldAddMetadata = addAliucordMetadata ?: false
|
||||
private var shouldAddMetadata = addManagerMetadata ?: false
|
||||
|
||||
override fun attr(ns: String?, name: String, resourceId: Int, type: Int, value: Any?) {
|
||||
if (name == NETWORK_SECURITY_CONFIG) return
|
||||
|
|
@ -142,10 +142,16 @@ object ManifestPatcher {
|
|||
override fun child(ns: String?, name: String): NodeVisitor {
|
||||
val visitor = super.child(ns, name)
|
||||
|
||||
if (shouldAddMetadata && addAliucordMetadata == true) {
|
||||
if (shouldAddMetadata && addManagerMetadata == true) {
|
||||
shouldAddMetadata = false
|
||||
super.child(ANDROID_NAMESPACE, "meta-data").apply {
|
||||
attr(ANDROID_NAMESPACE, "name", android.R.attr.name, TYPE_STRING, "isAliucord")
|
||||
attr(
|
||||
ANDROID_NAMESPACE,
|
||||
"name",
|
||||
android.R.attr.name,
|
||||
TYPE_STRING,
|
||||
"isWintry" /* originally "isAliucord" */
|
||||
)
|
||||
attr(ANDROID_NAMESPACE, "value", android.R.attr.value, TYPE_INT_BOOLEAN, 1)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,11 @@ private fun PatchOptionsScreenPreview(
|
|||
packageName = parameters.packageName,
|
||||
packageNameState = parameters.packageNameState,
|
||||
setPackageName = {},
|
||||
versionPreference = parameters.versionPreference,
|
||||
setVersionPreference = {},
|
||||
customVersionCode = parameters.customVersionCode,
|
||||
customVersionCodeIsError = parameters.customVersionCodeIsError,
|
||||
setCustomVersionCode = {},
|
||||
isConfigValid = parameters.isConfigValid,
|
||||
onInstall = {},
|
||||
)
|
||||
|
|
@ -50,6 +55,9 @@ private data class PatchOptionsParameters(
|
|||
val appNameIsError: Boolean,
|
||||
val packageName: String,
|
||||
val packageNameState: PackageNameState,
|
||||
val versionPreference: VersionPreference,
|
||||
val customVersionCode: String,
|
||||
val customVersionCodeIsError: Boolean,
|
||||
val isConfigValid: Boolean,
|
||||
)
|
||||
|
||||
|
|
@ -67,6 +75,9 @@ private class PatchOptionsParametersProvider : PreviewParameterProvider<PatchOpt
|
|||
appNameIsError = false,
|
||||
packageName = PatchOptions.Default.packageName,
|
||||
packageNameState = PackageNameState.Ok,
|
||||
versionPreference = VersionPreference.Stable,
|
||||
customVersionCode = "",
|
||||
customVersionCodeIsError = false,
|
||||
isConfigValid = true,
|
||||
),
|
||||
PatchOptionsParameters(
|
||||
|
|
@ -80,6 +91,9 @@ private class PatchOptionsParametersProvider : PreviewParameterProvider<PatchOpt
|
|||
appNameIsError = true,
|
||||
packageName = "a b",
|
||||
packageNameState = PackageNameState.Invalid,
|
||||
versionPreference = VersionPreference.Stable,
|
||||
customVersionCode = "",
|
||||
customVersionCodeIsError = false,
|
||||
isConfigValid = false,
|
||||
),
|
||||
PatchOptionsParameters(
|
||||
|
|
@ -93,6 +107,9 @@ private class PatchOptionsParametersProvider : PreviewParameterProvider<PatchOpt
|
|||
appNameIsError = false,
|
||||
packageName = PatchOptions.Default.packageName,
|
||||
packageNameState = PackageNameState.Taken,
|
||||
versionPreference = VersionPreference.Stable,
|
||||
customVersionCode = "",
|
||||
customVersionCodeIsError = false,
|
||||
isConfigValid = true,
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -16,13 +16,12 @@ import cafe.adriel.voyager.core.model.screenModelScope
|
|||
import com.aliucord.manager.BuildConfig
|
||||
import com.aliucord.manager.R
|
||||
import com.aliucord.manager.network.models.BuildInfo
|
||||
import com.aliucord.manager.network.services.AliucordGithubService
|
||||
import com.aliucord.manager.network.services.AliucordMavenService
|
||||
import com.aliucord.manager.network.models.RNATrackerIndex
|
||||
import com.aliucord.manager.network.services.*
|
||||
import com.aliucord.manager.network.utils.SemVer
|
||||
import com.aliucord.manager.network.utils.fold
|
||||
import com.aliucord.manager.patcher.InstallMetadata
|
||||
import com.aliucord.manager.ui.screens.patchopts.PatchOptions
|
||||
import com.aliucord.manager.ui.screens.patchopts.PatchOptionsScreen
|
||||
import com.aliucord.manager.ui.screens.patchopts.*
|
||||
import com.aliucord.manager.ui.util.DiscordVersion
|
||||
import com.aliucord.manager.ui.util.toUnsafeImmutable
|
||||
import com.aliucord.manager.util.*
|
||||
|
|
@ -38,6 +37,8 @@ class HomeModel(
|
|||
private val application: Application,
|
||||
private val github: AliucordGithubService,
|
||||
private val maven: AliucordMavenService,
|
||||
private val wtGithub: WintryGithubService,
|
||||
private val rnaTracker: RNATrackerService,
|
||||
private val json: Json,
|
||||
) : ScreenModel {
|
||||
var installsState by mutableStateOf<InstallsState>(InstallsState.Fetching)
|
||||
|
|
@ -45,6 +46,8 @@ class HomeModel(
|
|||
|
||||
private val refreshingLock = Mutex()
|
||||
private var remoteDataJson: BuildInfo? = null
|
||||
private var trackerIndexJson: RNATrackerIndex? = null
|
||||
private var latestWintryXposedVersion: SemVer? = null
|
||||
private var latestAliuhookVersion: SemVer? = null
|
||||
|
||||
init {
|
||||
|
|
@ -106,7 +109,7 @@ class HomeModel(
|
|||
val metadata = try {
|
||||
val applicationInfo = application.packageManager.getApplicationInfo(packageName, 0)
|
||||
val metadataFile = ZipReader(applicationInfo.publicSourceDir)
|
||||
.use { it.openEntry("aliucord.json")?.read() }
|
||||
.use { it.openEntry("wintry.json")?.read() }
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
metadataFile?.let { json.decodeFromStream<InstallMetadata>(it.inputStream()) }
|
||||
|
|
@ -187,21 +190,35 @@ class HomeModel(
|
|||
|
||||
private suspend fun fetchRemoteData() {
|
||||
listOf(
|
||||
// // These aren't needed by Wintry
|
||||
// screenModelScope.launch(Dispatchers.IO) {
|
||||
// github.getBuildData().fold(
|
||||
// success = { remoteDataJson = it },
|
||||
// fail = { Log.w(BuildConfig.TAG, "Failed to fetch remote build data", it) },
|
||||
// )
|
||||
// },
|
||||
// screenModelScope.launch(Dispatchers.IO) {
|
||||
// maven.getAliuhookVersion().fold(
|
||||
// success = { latestAliuhookVersion = it },
|
||||
// fail = { Log.w(BuildConfig.TAG, "Failed to fetch latest Aliuhook version", it) },
|
||||
// )
|
||||
// },
|
||||
screenModelScope.launch(Dispatchers.IO) {
|
||||
github.getBuildData().fold(
|
||||
success = { remoteDataJson = it },
|
||||
fail = { Log.w(BuildConfig.TAG, "Failed to fetch remote build data", it) },
|
||||
rnaTracker.getLatestDiscordVersions().fold(
|
||||
success = { trackerIndexJson = it },
|
||||
fail = { Log.w(BuildConfig.TAG, "Failed to fetch latest Discord RNA versions", it) },
|
||||
)
|
||||
},
|
||||
screenModelScope.launch(Dispatchers.IO) {
|
||||
maven.getAliuhookVersion().fold(
|
||||
success = { latestAliuhookVersion = it },
|
||||
fail = { Log.w(BuildConfig.TAG, "Failed to fetch latest Aliuhook version", it) },
|
||||
wtGithub.getLatestXposedRelease().fold(
|
||||
success = { latestWintryXposedVersion = SemVer.parse(it.name) },
|
||||
fail = { Log.w(BuildConfig.TAG, "Failed to fetch latest WintryXposed version", it) },
|
||||
)
|
||||
},
|
||||
|
||||
).joinAll()
|
||||
|
||||
if (remoteDataJson == null || latestAliuhookVersion == null) {
|
||||
if (trackerIndexJson == null /* remoteDataJson == null || latestAliuhookVersion == null */ || latestWintryXposedVersion == null) {
|
||||
mainThread { application.showToast(R.string.home_network_fail) }
|
||||
}
|
||||
}
|
||||
|
|
@ -213,34 +230,34 @@ class HomeModel(
|
|||
return application.packageManager
|
||||
.getInstalledPackages(PackageManager.GET_META_DATA)
|
||||
.filter {
|
||||
// Packages installed via the legacy Installer do not have the metadata marker
|
||||
val isAliucordPkg = it.packageName == "com.aliucord"
|
||||
val hasAliucordMeta = it.applicationInfo?.metaData?.containsKey("isAliucord") == true
|
||||
isAliucordPkg || hasAliucordMeta
|
||||
// Wintry doesn't have "legacy installer" whatsoever
|
||||
return@filter it.applicationInfo?.metaData?.containsKey("isWintry") == true
|
||||
|
||||
// // Packages installed via the legacy Installer do not have the metadata marker
|
||||
// val isAliucordPkg = it.packageName == "com.aliucord"
|
||||
// val hasAliucordMeta = it.applicationInfo?.metaData?.containsKey("isAliucord") == true
|
||||
// isAliucordPkg || hasAliucordMeta
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to determine whether the Aliucord installation is up-to-date.
|
||||
* If `null` is returned, then the build data was not fetched, and as such
|
||||
* the status cannot be determined.
|
||||
* Checks whether the current Wintry installation is up-to-date.
|
||||
*
|
||||
* Currently mirrors the behavior of Bunny Manager by directly comparing against
|
||||
* the latest available Discord and WintryXposed release. This is a temporary approach and will remain
|
||||
* in place until Wintry adds support for version pinning.
|
||||
*/
|
||||
private fun isInstallationUpToDate(pkg: PackageInfo): Boolean? {
|
||||
// Assume up-to-date when remote data hasn't been fetched yet
|
||||
val remoteBuildData = remoteDataJson ?: return null
|
||||
val latestAliuhookVersion = latestAliuhookVersion ?: return null
|
||||
val trackerData = trackerIndexJson ?: return null
|
||||
|
||||
// `longVersionCode` is unnecessary since Discord doesn't use `versionCodeMajor`
|
||||
@Suppress("DEPRECATION")
|
||||
val versionCode = pkg.versionCode
|
||||
|
||||
// Check if the base APK version is a mismatch
|
||||
if (remoteBuildData.discordVersionCode != versionCode) return false
|
||||
|
||||
// Try to parse install metadata. If none present, install was made via legacy installer.
|
||||
val apkPath = pkg.applicationInfo?.publicSourceDir ?: return false
|
||||
val installMetadata = try {
|
||||
val metadataFile = ZipReader(apkPath).use { it.openEntry("aliucord.json")?.read() }
|
||||
val metadataFile = ZipReader(apkPath).use { it.openEntry("wintry.json")?.read() }
|
||||
?: return false
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
|
|
@ -250,11 +267,13 @@ class HomeModel(
|
|||
return false
|
||||
}
|
||||
|
||||
// Check that all the installation components are up-to-date (skip not installed)
|
||||
val injectorUpToDate = installMetadata.injectorVersion == null || remoteBuildData.injectorVersion == installMetadata.injectorVersion
|
||||
val patchesUpToDate = installMetadata.patchesVersion == null || remoteBuildData.patchesVersion == installMetadata.patchesVersion
|
||||
val aliuhookUpToDate = installMetadata.aliuhookVersion == null || latestAliuhookVersion == installMetadata.aliuhookVersion
|
||||
val latestByPref = when (installMetadata.options.versionPreference) {
|
||||
VersionPreference.Stable -> trackerData.latest.stable
|
||||
VersionPreference.Beta -> trackerData.latest.beta
|
||||
VersionPreference.Alpha -> trackerData.latest.alpha
|
||||
VersionPreference.Custom -> return true
|
||||
}
|
||||
|
||||
return injectorUpToDate && patchesUpToDate && aliuhookUpToDate
|
||||
return latestByPref == versionCode && installMetadata.wintryXposedVersion == latestWintryXposedVersion
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -88,25 +88,21 @@ fun InstalledItemCard(
|
|||
horizontalArrangement = Arrangement.spacedBy(2.dp),
|
||||
modifier = Modifier.clip(MaterialTheme.shapes.large),
|
||||
) {
|
||||
SegmentedButton(
|
||||
icon = painterResource(R.drawable.ic_extension),
|
||||
text = stringResource(R.string.plugins_title),
|
||||
onClick = onOpenPlugins,
|
||||
)
|
||||
SegmentedButton(
|
||||
icon = painterResource(R.drawable.ic_info),
|
||||
text = stringResource(R.string.action_open_info),
|
||||
onClick = onOpenInfo,
|
||||
)
|
||||
// // Plugins cannot be managed externally yet in Wintry
|
||||
// SegmentedButton(
|
||||
// icon = painterResource(R.drawable.ic_extension),
|
||||
// text = stringResource(R.string.plugins_title),
|
||||
// onClick = onOpenPlugins,
|
||||
// )
|
||||
|
||||
// If the up-to-date status cannot be determined, assume it is up-to-date
|
||||
if (data.isUpToDate ?: true) {
|
||||
if (data.isUpToDate != false)
|
||||
SegmentedButton(
|
||||
icon = painterResource(R.drawable.ic_launch),
|
||||
text = stringResource(R.string.action_launch),
|
||||
onClick = onOpenApp,
|
||||
icon = painterResource(R.drawable.ic_refresh),
|
||||
text = stringResource(R.string.reinstall),
|
||||
onClick = onUpdate,
|
||||
)
|
||||
} else {
|
||||
else {
|
||||
val warningColor = Color(0xFFFFBB33)
|
||||
|
||||
SegmentedButton(
|
||||
|
|
@ -117,6 +113,16 @@ fun InstalledItemCard(
|
|||
onClick = onUpdate,
|
||||
)
|
||||
}
|
||||
SegmentedButton(
|
||||
icon = painterResource(R.drawable.ic_info),
|
||||
text = stringResource(R.string.action_open_info),
|
||||
onClick = onOpenInfo,
|
||||
)
|
||||
SegmentedButton(
|
||||
icon = painterResource(R.drawable.ic_launch),
|
||||
text = stringResource(R.string.action_launch),
|
||||
onClick = onOpenApp,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,13 @@ import kotlinx.parcelize.TypeParceler
|
|||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
enum class VersionPreference {
|
||||
Stable,
|
||||
Beta,
|
||||
Alpha,
|
||||
Custom;
|
||||
}
|
||||
|
||||
@Immutable
|
||||
@Parcelize
|
||||
@Serializable
|
||||
|
|
@ -33,6 +40,16 @@ data class PatchOptions(
|
|||
* Replacement of the user-facing launcher icon.
|
||||
*/
|
||||
val iconReplacement: IconReplacement,
|
||||
|
||||
/**
|
||||
* Version preference to fetch and install
|
||||
*/
|
||||
val versionPreference: VersionPreference,
|
||||
|
||||
/**
|
||||
* Version to fetch and install when versionPreference is set to VersionPreference.Custom.
|
||||
*/
|
||||
val customVersionCode: String,
|
||||
) : Parcelable {
|
||||
@Immutable
|
||||
@Parcelize
|
||||
|
|
@ -109,6 +126,8 @@ data class PatchOptions(
|
|||
packageName = "com.aliucord",
|
||||
debuggable = false,
|
||||
iconReplacement = IconReplacement.CustomColor(IconReplacement.AliucordColor),
|
||||
versionPreference = VersionPreference.Stable,
|
||||
customVersionCode = "126021",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import androidx.core.content.getSystemService
|
|||
import cafe.adriel.voyager.core.model.ScreenModel
|
||||
import cafe.adriel.voyager.core.model.screenModelScope
|
||||
import com.aliucord.manager.manager.PreferencesManager
|
||||
import com.aliucord.manager.ui.util.DiscordVersion
|
||||
import com.aliucord.manager.util.*
|
||||
|
||||
class PatchOptionsModel(
|
||||
|
|
@ -48,6 +49,26 @@ class PatchOptionsModel(
|
|||
debuggable = value
|
||||
}
|
||||
|
||||
// ---------- Version preference state ----------
|
||||
var versionPreference by mutableStateOf(prefilledOptions.versionPreference)
|
||||
private set
|
||||
|
||||
fun changeVersionPreference(value: VersionPreference) {
|
||||
versionPreference = value
|
||||
}
|
||||
|
||||
// ---------- Custom version code state ----------
|
||||
var customVersionCode by mutableStateOf(prefilledOptions.customVersionCode)
|
||||
private set
|
||||
|
||||
var customVersionCodeIsError by mutableStateOf(false)
|
||||
private set
|
||||
|
||||
fun changeCustomVersionCode(value: String) {
|
||||
customVersionCode = value
|
||||
customVersionCodeIsError = !DiscordVersion.isValid(value)
|
||||
}
|
||||
|
||||
// ---------- Other ----------
|
||||
var showNetworkWarningDialog by mutableStateOf(!alreadyShownNetworkWarning && isNetworkDangerous())
|
||||
private set
|
||||
|
|
@ -76,6 +97,8 @@ class PatchOptionsModel(
|
|||
packageName = packageName,
|
||||
debuggable = debuggable,
|
||||
iconReplacement = icon,
|
||||
versionPreference = versionPreference,
|
||||
customVersionCode = customVersionCode,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.aliucord.manager.ui.screens.patchopts
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
|
|
@ -30,8 +31,7 @@ import com.aliucord.manager.ui.screens.patching.PatchingScreen
|
|||
import com.aliucord.manager.ui.screens.patchopts.components.PackageNameStateLabel
|
||||
import com.aliucord.manager.ui.screens.patchopts.components.PatchOptionsAppBar
|
||||
import com.aliucord.manager.ui.screens.patchopts.components.options.*
|
||||
import com.aliucord.manager.ui.util.InstallNotifications
|
||||
import com.aliucord.manager.ui.util.spacedByLastAtBottom
|
||||
import com.aliucord.manager.ui.util.*
|
||||
import com.aliucord.manager.util.*
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
|
|
@ -101,6 +101,13 @@ class PatchOptionsScreen(
|
|||
packageNameState = model.packageNameState,
|
||||
setPackageName = model::changePackageName,
|
||||
|
||||
versionPreference = model.versionPreference,
|
||||
setVersionPreference = model::changeVersionPreference,
|
||||
|
||||
customVersionCode = model.customVersionCode,
|
||||
customVersionCodeIsError = model.customVersionCodeIsError,
|
||||
setCustomVersionCode = model::changeCustomVersionCode,
|
||||
|
||||
isConfigValid = model.isConfigValid,
|
||||
onInstall = onInstall@{
|
||||
val iconConfig = iconModel.generateConfig()
|
||||
|
|
@ -137,6 +144,13 @@ fun PatchOptionsScreenContent(
|
|||
packageNameState: PackageNameState,
|
||||
setPackageName: (String) -> Unit,
|
||||
|
||||
versionPreference: VersionPreference,
|
||||
setVersionPreference: (VersionPreference) -> Unit,
|
||||
|
||||
customVersionCode: String,
|
||||
customVersionCodeIsError: Boolean,
|
||||
setCustomVersionCode: (String) -> Unit,
|
||||
|
||||
isConfigValid: Boolean,
|
||||
onInstall: () -> Unit,
|
||||
) {
|
||||
|
|
@ -236,6 +250,40 @@ fun PatchOptionsScreenContent(
|
|||
)
|
||||
}
|
||||
|
||||
SelectPatchOption(
|
||||
icon = painterResource(R.drawable.ic_app_shortcut),
|
||||
name = "Preferred version",
|
||||
description = "Choose which release channel or input a custom version code to use for installation.",
|
||||
options = VersionPreference.entries.map { it.name },
|
||||
selectedOption = versionPreference.name,
|
||||
onOptionSelected = { selected ->
|
||||
setVersionPreference(VersionPreference.valueOf(selected))
|
||||
}
|
||||
)
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = versionPreference == VersionPreference.Custom,
|
||||
label = "Custom version code option visibility"
|
||||
) {
|
||||
TextPatchOption(
|
||||
name = "Custom version code",
|
||||
description = "Input a custom version code to use for installation.",
|
||||
value = customVersionCode,
|
||||
valueIsError = customVersionCodeIsError,
|
||||
valueIsDefault = customVersionCode == PatchOptions.Default.customVersionCode,
|
||||
onValueChange = setCustomVersionCode,
|
||||
onValueReset = {
|
||||
setCustomVersionCode(PatchOptions.Default.customVersionCode)
|
||||
}
|
||||
) {
|
||||
VersionDisplay(
|
||||
version = DiscordVersion.parseFromString(customVersionCode),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
modifier = Modifier.padding(start = 4.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (isDevMode) {
|
||||
TextDivider(
|
||||
text = stringResource(R.string.patchopts_divider_advanced),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,64 @@
|
|||
package com.aliucord.manager.ui.screens.patchopts.components.options
|
||||
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
|
||||
@Composable
|
||||
fun SelectPatchOption(
|
||||
icon: Painter,
|
||||
name: String,
|
||||
description: String,
|
||||
options: List<String>,
|
||||
selectedOption: String,
|
||||
onOptionSelected: (String) -> Unit,
|
||||
enabled: Boolean = true,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
val interactionSource = remember(::MutableInteractionSource)
|
||||
|
||||
IconPatchOption(
|
||||
icon = icon,
|
||||
name = name,
|
||||
description = description,
|
||||
modifier = modifier.fillMaxWidth()
|
||||
) {
|
||||
FilledTonalButton(
|
||||
onClick = { if (enabled) expanded = true },
|
||||
enabled = enabled,
|
||||
interactionSource = interactionSource,
|
||||
) {
|
||||
Text(
|
||||
text = selectedOption,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
DropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = { expanded = false },
|
||||
) {
|
||||
options.forEach { option ->
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(
|
||||
text = option,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
expanded = false
|
||||
onOptionSelected(option)
|
||||
},
|
||||
enabled = enabled,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -10,6 +10,9 @@ import kotlinx.parcelize.Parcelize
|
|||
@Parcelize
|
||||
@Immutable
|
||||
sealed interface DiscordVersion : Comparable<DiscordVersion>, Parcelable {
|
||||
@Parcelize
|
||||
data object Invalid : DiscordVersion
|
||||
|
||||
@Parcelize
|
||||
data object Error : DiscordVersion
|
||||
|
||||
|
|
@ -31,6 +34,7 @@ sealed interface DiscordVersion : Comparable<DiscordVersion>, Parcelable {
|
|||
return when (this) {
|
||||
is Error -> 0
|
||||
is None -> 0
|
||||
is Invalid -> 0
|
||||
is Existing -> {
|
||||
if (other is Existing) {
|
||||
other.typelessCode.compareTo(typelessCode)
|
||||
|
|
@ -44,6 +48,7 @@ sealed interface DiscordVersion : Comparable<DiscordVersion>, Parcelable {
|
|||
@Composable
|
||||
fun toDisplayName() = when (this) {
|
||||
is Error -> stringResource(R.string.version_load_fail)
|
||||
is Invalid -> stringResource(R.string.invalid_version_code_format)
|
||||
is None -> stringResource(R.string.version_none)
|
||||
is Existing -> when (type) {
|
||||
Type.STABLE -> stringResource(R.string.version_stable)
|
||||
|
|
@ -61,6 +66,23 @@ sealed interface DiscordVersion : Comparable<DiscordVersion>, Parcelable {
|
|||
}
|
||||
|
||||
companion object {
|
||||
val DISCORD_VERSION_REGEX = Regex("""^\d{3}[0-2]\d{2}$""")
|
||||
|
||||
fun parseFromString(version: String): DiscordVersion {
|
||||
if (!DISCORD_VERSION_REGEX.matches(version)) {
|
||||
return Invalid
|
||||
}
|
||||
|
||||
val versionCode = version.toInt()
|
||||
val type = parseVersionType(versionCode)
|
||||
|
||||
return Existing(
|
||||
type = type,
|
||||
name = "${versionCode / 1000}.${versionCode % 100}",
|
||||
code = versionCode,
|
||||
)
|
||||
}
|
||||
|
||||
fun parseVersionType(versionCode: Int?): Type {
|
||||
return when (versionCode?.div(100)?.mod(10)) {
|
||||
0 -> Type.STABLE
|
||||
|
|
@ -69,5 +91,9 @@ sealed interface DiscordVersion : Comparable<DiscordVersion>, Parcelable {
|
|||
else -> Type.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
fun isValid(versionCode: String): Boolean {
|
||||
return DISCORD_VERSION_REGEX.matches(versionCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -252,4 +252,8 @@
|
|||
<string name="downloading_resources_apk">Downloading resources apk</string>
|
||||
<string name="injecting_wintryxposed_with_lspatch">Injecting WintryXposed with LSPatch</string>
|
||||
<string name="downloading_wintryxposed">Downloading WintryXposed</string>
|
||||
<string name="version_code">Version Code</string>
|
||||
<string name="fetching_latest_discord_info">Fetching latest Discord info</string>
|
||||
<string name="invalid_version_code_format">Invalid version code format</string>
|
||||
<string name="reinstall">Reinstall</string>
|
||||
</resources>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue