Manager UI Updates & New patch logic <3

This commit is contained in:
2026-05-24 01:20:42 +10:00
parent 6a9b3f9d05
commit 691bcaa2b8
24 changed files with 229 additions and 205 deletions
@@ -150,15 +150,13 @@ class PatchingScreenModel(
.toUnsafeImmutable()
mainThread { steps = newSteps }
// Intentionally delay to show the state change of the first step when it runs in the UI.
// Without this, on a fast internet connection the step just immediately shows as "Success".
// Tiny delay so the first step's progress is visible on fast connections
delay(400)
// Execute all the steps and catch any errors
val error = when (val error = runner.executeAll()) {
null -> {
// If install step is marked skipped then the installation was manually aborted
// and if so, immediately close install screen
// Skipped install step means user aborted, close screen
if (runner.getStep<InstallStep>().state == StepState.Skipped) {
mutableState.value = PatchingScreenState.CloseScreen
@@ -215,6 +213,7 @@ class PatchingScreenModel(
R.string.fun_fact_10,
R.string.fun_fact_11,
R.string.fun_fact_12,
R.string.fun_fact_13,
)
}
}
@@ -8,37 +8,64 @@ enum class KnownPatch(
@StringRes val titleRes: Int,
@StringRes val descRes: Int,
val requires: List<KnownPatch> = emptyList(),
val disables: List<KnownPatch> = emptyList(),
) {
// Dependency-first order (later refs need backward resolution)
LyricsDisableCover(
fileNames = listOf("lyrics-disable-cover.patch"),
titleRes = R.string.patch_lyrics_disable_cover_title,
descRes = R.string.patch_lyrics_disable_cover_desc,
),
LyricsProgressPill(
LyricsReplaceLyricsButton(
fileNames = listOf(
"lyrics-progress-pill.patch",
"lyrics-fade-region.patch",
"lyrics-active-line-only.patch",
),
titleRes = R.string.patch_lyrics_progress_pill_title,
descRes = R.string.patch_lyrics_progress_pill_desc,
requires = listOf(LyricsDisableCover),
),
LyricsReplaceLyricButton(
fileNames = listOf(
"lyrics-replace-lyric-button-1-remove.patch",
"lyrics-replace-lyric-button-2-sparkle.patch",
"lyrics-replace-lyrics-button.patch",
"lyrics-sparkle-conditional-visibility.patch",
),
titleRes = R.string.patch_lyrics_replace_button_title,
descRes = R.string.patch_lyrics_replace_button_desc,
),
LyricsReplaceShareButton(
fileNames = listOf("lyrics-replace-share-button.patch"),
titleRes = R.string.patch_lyrics_replace_share_button_title,
descRes = R.string.patch_lyrics_replace_share_button_desc,
),
PlayerBackdrop(
fileNames = listOf("player-backdrop.patch"),
titleRes = R.string.patch_player_backdrop_title,
descRes = R.string.patch_player_backdrop_desc,
),
DebugMenuUnlock(
fileNames = listOf("debug-menu-unlock.patch"),
titleRes = R.string.patch_debug_menu_unlock_title,
descRes = R.string.patch_debug_menu_unlock_desc,
),
LyricsProgressPill(
fileNames = listOf(
"lyrics-progress-pill.patch",
"lyrics-fade-region.patch",
),
titleRes = R.string.patch_lyrics_progress_pill_title,
descRes = R.string.patch_lyrics_progress_pill_desc,
requires = listOf(LyricsDisableCover, LyricsReplaceShareButton),
),
EnableLegacyUi(
fileNames = listOf("enable-legacy-ui.patch"),
titleRes = R.string.patch_enable_legacy_ui_title,
descRes = R.string.patch_enable_legacy_ui_desc,
requires = listOf(DebugMenuUnlock),
disables = listOf(
LyricsDisableCover,
LyricsReplaceLyricsButton,
LyricsReplaceShareButton,
PlayerBackdrop,
LyricsProgressPill,
),
);
companion object {
val All: List<KnownPatch> = entries.sortedBy { it.fileNames.first() }
// Alphabetical by first filename, but pin DebugMenuUnlock to the bottom
val All: List<KnownPatch> = entries.sortedWith(
compareBy({ it == DebugMenuUnlock }, { it.fileNames.first() })
)
}
}
@@ -18,7 +18,6 @@ class PatchOptionsModel(
private val context: Context,
private val prefs: PreferencesManager,
) : ScreenModel {
// ---------- Package name state ----------
var packageName by mutableStateOf(prefilledOptions.packageName)
private set
@@ -30,7 +29,6 @@ class PatchOptionsModel(
fetchPkgNameStateDebounced()
}
// ---------- App name state ----------
var appName by mutableStateOf(prefilledOptions.appName)
private set
@@ -42,7 +40,6 @@ class PatchOptionsModel(
appNameIsError = newAppName.length !in (1..150)
}
// ---------- Debuggable state ----------
var debuggable by mutableStateOf(prefilledOptions.debuggable)
private set
@@ -50,7 +47,6 @@ class PatchOptionsModel(
debuggable = value
}
// ---------- Patch selection state ----------
var disabledPatches by mutableStateOf(prefilledOptions.disabledPatches)
private set
@@ -58,38 +54,33 @@ class PatchOptionsModel(
patch.fileNames.none { it in disabledPatches }
fun setPatchEnabled(patch: KnownPatch, enabled: Boolean) {
val units = if (enabled) {
fun closure(seed: KnownPatch, step: (KnownPatch) -> List<KnownPatch>): Set<KnownPatch> =
buildSet {
fun addWithDeps(p: KnownPatch) {
if (add(p)) p.requires.forEach(::addWithDeps)
}
addWithDeps(patch)
fun walk(p: KnownPatch) { if (add(p)) step(p).forEach(::walk) }
walk(seed)
}
val enableUnits: Set<KnownPatch>
val disableUnits: Set<KnownPatch>
if (enabled) {
enableUnits = closure(patch) { it.requires }
disableUnits = enableUnits.flatMap { it.disables }
.flatMapTo(mutableSetOf()) { d ->
closure(d) { dep -> KnownPatch.All.filter { dep in it.requires } }
}
} else {
buildSet {
fun addWithDependents(p: KnownPatch) {
if (add(p)) {
KnownPatch.All
.filter { p in it.requires }
.forEach(::addWithDependents)
}
}
addWithDependents(patch)
}
enableUnits = emptySet()
disableUnits = closure(patch) { p -> KnownPatch.All.filter { p in it.requires } }
}
val affectedFiles = units.flatMap { it.fileNames }.toSet()
disabledPatches = if (enabled) {
disabledPatches - affectedFiles
} else {
disabledPatches + affectedFiles
}
val enableFiles = enableUnits.flatMap { it.fileNames }.toSet()
val disableFiles = disableUnits.flatMap { it.fileNames }.toSet()
disabledPatches = (disabledPatches - enableFiles) + disableFiles
}
val enabledPatchCount: Int
get() = KnownPatch.All.count { isPatchEnabled(it) }
// ---------- Custom components state ----------
var customInjector by mutableStateOf<PatchComponent?>(null)
private set
var customPatches by mutableStateOf<PatchComponent?>(null)
@@ -113,7 +104,6 @@ class PatchOptionsModel(
)
}
// ---------- Config generation ----------
val isConfigValid by derivedStateOf {
val invalidChecks = arrayOf(
packageNameState == PackageNameState.Invalid,
@@ -136,11 +126,9 @@ class PatchOptionsModel(
)
}
// ---------- Other ----------
val isDevMode: Boolean
get() = prefs.devMode
// A throttled variant of fetchPkgNameState()
private val fetchPkgNameStateDebounced: () -> Unit =
screenModelScope.debounce(100L, function = ::fetchPkgNameState)
@@ -1,5 +1,6 @@
package com.meowarex.rlmobile.ui.screens.patchopts.components
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.background
@@ -62,7 +62,7 @@ fun InstallersDialog(
}
InstallerSetting.Intent -> {
// We don't know whether this device supports this method until we try.
// don't know whether this device supports this method
}
InstallerSetting.Shizuku -> {
@@ -147,7 +147,7 @@ class UpdaterViewModel(
// Fetch releases from GitHub (60s local cache)
val releases = github.getManagerReleases().getOrThrow()
// Find the latest release — version is parsed from the release title (e.g. "v1.0.5")
// Find the latest release by parsed version
val (version, release, apkUrl) = releases
.mapNotNull { release ->
val version = SemVer.parseOrNull(release.name?.removePrefix("v") ?: "")