Custom Patch Selection

This commit is contained in:
2026-05-21 20:50:15 +10:00
parent e5f186b264
commit c3d9ce2a39
15 changed files with 429 additions and 36 deletions
@@ -34,6 +34,9 @@ private fun PatchOptionsScreenPreview(
onSelectCustomInjector = {},
customPatches = parameters.customPatches,
onSelectCustomPatches = {},
enabledPatchCount = KnownPatch.All.size,
isPatchEnabled = { true },
onTogglePatch = { _, _ -> },
isConfigValid = parameters.isConfigValid,
onInstall = {},
)
@@ -87,9 +87,9 @@ class PatchingScreenModel(
}
}
fun clearCache() = screenModelScope.launchIO {
fun clearCache() {
paths.clearCache()
mainThread { application.showToast(R.string.action_cleared_cache) }
application.showToast(R.string.action_cleared_cache)
}
fun getCurrentInstallId(): String? = installId
@@ -0,0 +1,44 @@
package com.meowarex.rlmobile.ui.screens.patchopts
import androidx.annotation.StringRes
import com.meowarex.rlmobile.R
enum class KnownPatch(
val fileNames: List<String>,
@StringRes val titleRes: Int,
@StringRes val descRes: Int,
val requires: List<KnownPatch> = emptyList(),
) {
LyricsDisableCover(
fileNames = listOf("lyrics-disable-cover.patch"),
titleRes = R.string.patch_lyrics_disable_cover_title,
descRes = R.string.patch_lyrics_disable_cover_desc,
),
LyricsProgressPill(
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",
),
titleRes = R.string.patch_lyrics_replace_button_title,
descRes = R.string.patch_lyrics_replace_button_desc,
),
PlayerBackdrop(
fileNames = listOf("player-backdrop.patch"),
titleRes = R.string.patch_player_backdrop_title,
descRes = R.string.patch_player_backdrop_desc,
);
companion object {
val All: List<KnownPatch> = entries.sortedBy { it.fileNames.first() }
}
}
@@ -34,6 +34,8 @@ data class PatchOptions(
* A custom smali patches bundle that was used rather than the latest.
*/
val customPatches: PatchComponent? = null,
val disabledPatches: Set<String> = emptySet(),
) : Parcelable {
companion object {
val Default = PatchOptions(
@@ -42,6 +44,7 @@ data class PatchOptions(
debuggable = false,
customInjector = null,
customPatches = null,
disabledPatches = emptySet(),
)
}
}
@@ -50,6 +50,45 @@ class PatchOptionsModel(
debuggable = value
}
// ---------- Patch selection state ----------
var disabledPatches by mutableStateOf(prefilledOptions.disabledPatches)
private set
fun isPatchEnabled(patch: KnownPatch): Boolean =
patch.fileNames.none { it in disabledPatches }
fun setPatchEnabled(patch: KnownPatch, enabled: Boolean) {
val units = if (enabled) {
buildSet {
fun addWithDeps(p: KnownPatch) {
if (add(p)) p.requires.forEach(::addWithDeps)
}
addWithDeps(patch)
}
} else {
buildSet {
fun addWithDependents(p: KnownPatch) {
if (add(p)) {
KnownPatch.All
.filter { p in it.requires }
.forEach(::addWithDependents)
}
}
addWithDependents(patch)
}
}
val affectedFiles = units.flatMap { it.fileNames }.toSet()
disabledPatches = if (enabled) {
disabledPatches - affectedFiles
} else {
disabledPatches + affectedFiles
}
}
val enabledPatchCount: Int
get() = KnownPatch.All.count { isPatchEnabled(it) }
// ---------- Custom components state ----------
var customInjector by mutableStateOf<PatchComponent?>(null)
private set
@@ -93,6 +132,7 @@ class PatchOptionsModel(
debuggable = debuggable,
customInjector = customInjector,
customPatches = customPatches,
disabledPatches = disabledPatches,
)
}
@@ -21,6 +21,7 @@ import com.meowarex.rlmobile.ui.screens.componentopts.PatchComponent
import com.meowarex.rlmobile.ui.screens.patching.PatchingScreen
import com.meowarex.rlmobile.ui.screens.patchopts.components.PackageNameStateLabel
import com.meowarex.rlmobile.ui.screens.patchopts.components.PatchOptionsAppBar
import com.meowarex.rlmobile.ui.screens.patchopts.components.PatchSelectionAccordion
import com.meowarex.rlmobile.ui.screens.patchopts.components.options.*
import com.meowarex.rlmobile.ui.util.spacedByLastAtBottom
import kotlinx.parcelize.IgnoredOnParcel
@@ -59,6 +60,10 @@ class PatchOptionsScreen(
onSelectCustomInjector = { model.selectCustomInjector(navigator) },
onSelectCustomPatches = { model.selectCustomPatches(navigator) },
enabledPatchCount = model.enabledPatchCount,
isPatchEnabled = model::isPatchEnabled,
onTogglePatch = model::setPatchEnabled,
isConfigValid = model.isConfigValid,
onInstall = {
navigator.push(PatchingScreen(model.generateConfig()))
@@ -88,6 +93,10 @@ fun PatchOptionsScreenContent(
customPatches: PatchComponent?,
onSelectCustomPatches: () -> Unit,
enabledPatchCount: Int,
isPatchEnabled: (KnownPatch) -> Boolean,
onTogglePatch: (KnownPatch, Boolean) -> Unit,
isConfigValid: Boolean,
onInstall: () -> Unit,
) {
@@ -148,6 +157,14 @@ fun PatchOptionsScreenContent(
}
}
PatchSelectionAccordion(
enabledCount = enabledPatchCount,
totalCount = KnownPatch.All.size,
isEnabled = isPatchEnabled,
onToggle = onTogglePatch,
modifier = Modifier.padding(top = 4.dp),
)
if (isDevMode) {
TextDivider(
text = stringResource(R.string.patchopts_divider_advanced),
@@ -0,0 +1,150 @@
package com.meowarex.rlmobile.ui.screens.patchopts.components
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
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.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.dp
import com.meowarex.rlmobile.R
import com.meowarex.rlmobile.ui.screens.patchopts.KnownPatch
@Composable
fun PatchSelectionAccordion(
enabledCount: Int,
totalCount: Int,
isEnabled: (KnownPatch) -> Boolean,
onToggle: (KnownPatch, Boolean) -> Unit,
modifier: Modifier = Modifier,
) {
var expanded by rememberSaveable { mutableStateOf(false) }
val rotation by animateFloatAsState(
targetValue = if (expanded) 180f else 0f,
label = "patch-accordion-arrow",
)
Column(
modifier = modifier
.fillMaxWidth()
.clip(MaterialTheme.shapes.large)
.background(MaterialTheme.colorScheme.surfaceContainerLow),
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
modifier = Modifier
.fillMaxWidth()
.clickable(role = Role.Button) { expanded = !expanded }
.padding(horizontal = 16.dp, vertical = 14.dp),
) {
Column(
verticalArrangement = Arrangement.spacedBy(2.dp),
modifier = Modifier.weight(1f),
) {
Text(
text = stringResource(R.string.patchopts_patches_title),
style = MaterialTheme.typography.titleMedium,
)
Text(
text = stringResource(
R.string.patchopts_patches_summary,
enabledCount,
totalCount,
),
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.alpha(.7f),
)
}
Icon(
painter = painterResource(R.drawable.ic_arrow_down_small),
contentDescription = stringResource(
if (expanded) R.string.action_collapse else R.string.action_expand,
),
modifier = Modifier.rotate(rotation),
)
}
AnimatedVisibility(visible = expanded) {
Column(
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.background.copy(0.4f))
.padding(horizontal = 16.dp, vertical = 12.dp),
) {
Text(
text = stringResource(R.string.patchopts_patches_desc),
style = MaterialTheme.typography.bodySmall,
modifier = Modifier
.alpha(.7f)
.padding(bottom = 8.dp),
)
for (patch in KnownPatch.All) key(patch) {
PatchCheckboxRow(
title = stringResource(patch.titleRes),
description = stringResource(patch.descRes),
checked = isEnabled(patch),
onCheckedChange = { onToggle(patch, it) },
)
}
}
}
}
}
@Composable
private fun PatchCheckboxRow(
title: String,
description: String,
checked: Boolean,
onCheckedChange: (Boolean) -> Unit,
) {
val interactionSource = remember(::MutableInteractionSource)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier
.fillMaxWidth()
.clickable(
interactionSource = interactionSource,
indication = null,
role = Role.Checkbox,
) { onCheckedChange(!checked) }
.padding(vertical = 4.dp),
) {
Checkbox(
checked = checked,
onCheckedChange = onCheckedChange,
interactionSource = interactionSource,
)
Column(
verticalArrangement = Arrangement.spacedBy(2.dp),
modifier = Modifier.weight(1f),
) {
Text(
text = title,
style = MaterialTheme.typography.titleSmall,
)
Text(
text = description,
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.alpha(.7f),
)
}
}
}
@@ -64,13 +64,10 @@ class SettingsModel(
if (value) UpdateCheckWorker.schedule(application) else UpdateCheckWorker.cancel(application)
}
fun clearCache() = screenModelScope.launchIO {
fun clearCache() {
paths.clearCache()
mainThread {
patchedApkExists = false
application.showToast(R.string.action_cleared_cache)
}
patchedApkExists = false
application.showToast(R.string.action_cleared_cache)
}
fun copyInstallInfo() {