feat(patch): mini player gestures patch + patch suboptions

This commit is contained in:
Se7en-dev
2026-06-23 14:18:38 +02:00
parent 7a55787894
commit 3788eb2008
17 changed files with 2350 additions and 0 deletions
@@ -42,6 +42,8 @@ class SmaliPatchStep(
val patches = mutableListOf<LoadedPatch>()
val localsBumps = mutableMapOf<Pair<String, String>, Int>()
val disabledFiles = options.disabledPatchFiles()
val knownExtensionFiles = options.knownExtensionFiles()
val enabledExtensionFiles = options.enabledExtensionFiles()
// Load and parse all the patches from the smali archive.
container.log("Loading patches from smali patch archive: ${patchesZip.absolutePath}")
@@ -54,6 +56,11 @@ class SmaliPatchStep(
if (patchFile.endsWith(".smali") && patchFile.startsWith("extension/")) {
val relative = patchFile.removePrefix("extension/")
if (relative in knownExtensionFiles && relative !in enabledExtensionFiles) {
container.log("Skipping disabled extension smali: $relative")
continue
}
val out = smaliDir.resolve(relative)
// Guard against zip-slip: a crafted entry could otherwise escape smaliDir.
val baseCanonical = smaliDir.canonicalPath + File.separator
@@ -40,6 +40,8 @@ private fun PatchOptionsScreenPreview(
patchLockState = { PatchLock.Free },
variantIndex = { 0 },
onSelectVariant = { _, _ -> },
isSubOptionEnabled = { _, _ -> true },
onToggleSubOption = { _, _, _ -> },
isConfigValid = parameters.isConfigValid,
onInstall = {},
)
@@ -8,12 +8,23 @@ import com.meowarex.rlmobile.ui.screens.patchopts.PatchDefault.Enabled
data class PatchVariant(
@StringRes val titleRes: Int,
val fileNames: List<String>,
val extensionFileNames: List<String> = emptyList(),
)
data class PatchSubOption(
val key: String,
@StringRes val titleRes: Int,
@StringRes val descRes: Int,
val fileNames: List<String>,
val default: PatchDefault,
val extensionFileNames: List<String> = emptyList(),
)
enum class KnownPatch(
val order: Int, // Patch order in the UI List (lower = higher up) [Main Patches: multiples of 10 | Sub Patches: multiples of 1]
val fileNames: List<String>,
val extensionFileNames: List<String> = emptyList(),
@StringRes val titleRes: Int,
@StringRes val descRes: Int,
val default: PatchDefault, // Default state of the patch in the UI List (enabled/disabled)
@@ -21,6 +32,7 @@ enum class KnownPatch(
val disables: List<KnownPatch> = emptyList(),
val variants: List<PatchVariant> = emptyList(),
val defaultVariantIndex: Int = 0,
val subOptions: List<PatchSubOption> = emptyList(),
) {
LyricsDisableCover(
order = 41,
@@ -135,6 +147,33 @@ enum class KnownPatch(
),
),
),
MiniPlayerGestures(
order = 51,
fileNames = listOf("mini-player-gestures.patch"),
extensionFileNames = listOf(
"radiant/MiniPlayerGestures.smali",
"radiant/MiniPlayerGestures\$Gesture.smali",
"radiant/MiniPlayerGestures\$RootGesture.smali",
"radiant/MiniPlayerGestures\$ApplyPending.smali",
),
titleRes = R.string.patch_mini_player_gestures_title,
descRes = R.string.patch_mini_player_gestures_desc,
default = Enabled,
subOptions = listOf(
PatchSubOption(
key = "MiniPlayerGestures.LeftRight",
titleRes = R.string.patch_mini_player_left_right_gestures_title,
descRes = R.string.patch_mini_player_left_right_gestures_desc,
fileNames = listOf("mini-player-gestures-left-right.patch"),
default = Disabled,
extensionFileNames = listOf(
"radiant/MiniPlayerTrackGestures.smali",
"radiant/MiniPlayerTrackGestures\$Gesture.smali",
"com/tidal/android/feature/appscaffold/ui/q\$c.smali",
),
),
),
),
EnableLegacyUi(
order = 10,
fileNames = listOf("enable-legacy-ui.patch"),
@@ -152,6 +191,7 @@ enum class KnownPatch(
QualityBadgeColors,
LyricsProgressPill,
CoverEverywhere,
MiniPlayerGestures,
),
);
@@ -40,6 +40,9 @@ data class PatchOptions(
fun isEnabled(patch: KnownPatch): Boolean =
patchStates[patch.name] ?: patch.default.isEnabled
fun isEnabled(subOption: PatchSubOption): Boolean =
patchStates[subOption.key] ?: subOption.default.isEnabled
fun disabledPatchFiles(): Set<String> = buildSet<String> {
for (patch in KnownPatch.All) {
val enabled = isEnabled(patch)
@@ -52,6 +55,35 @@ data class PatchOptions(
if (!enabled || index != selected) addAll(variant.fileNames)
}
}
for (subOption in patch.subOptions) {
if (!enabled || !isEnabled(subOption)) addAll(subOption.fileNames)
}
}
}
fun knownExtensionFiles(): Set<String> = buildSet {
for (patch in KnownPatch.All) {
addAll(patch.extensionFileNames)
patch.variants.forEach { addAll(it.extensionFileNames) }
patch.subOptions.forEach { addAll(it.extensionFileNames) }
}
}
fun enabledExtensionFiles(): Set<String> = buildSet {
for (patch in KnownPatch.All) {
if (!isEnabled(patch)) continue
addAll(patch.extensionFileNames)
if (patch.variants.isNotEmpty()) {
val selected = (selectedVariants[patch.name] ?: patch.defaultVariantIndex)
.coerceIn(0, patch.variants.lastIndex)
addAll(patch.variants[selected].extensionFileNames)
}
for (subOption in patch.subOptions) {
if (isEnabled(subOption)) addAll(subOption.extensionFileNames)
}
}
}
@@ -60,6 +60,14 @@ class PatchOptionsModel(
fun isPatchEnabled(patch: KnownPatch): Boolean =
patchStates[patch.name] ?: patch.default.isEnabled
fun isSubOptionEnabled(patch: KnownPatch, subOption: PatchSubOption): Boolean =
isPatchEnabled(patch) && (patchStates[subOption.key] ?: subOption.default.isEnabled)
fun setSubOptionEnabled(patch: KnownPatch, subOption: PatchSubOption, enabled: Boolean) {
if (subOption !in patch.subOptions) return
patchStates = patchStates + (subOption.key to enabled)
}
fun setPatchEnabled(patch: KnownPatch, enabled: Boolean) {
fun closure(seed: KnownPatch, step: (KnownPatch) -> List<KnownPatch>): Set<KnownPatch> =
buildSet {
@@ -66,6 +66,8 @@ class PatchOptionsScreen(
patchLockState = model::lockState,
variantIndex = model::variantIndex,
onSelectVariant = model::selectVariant,
isSubOptionEnabled = model::isSubOptionEnabled,
onToggleSubOption = model::setSubOptionEnabled,
isConfigValid = model.isConfigValid,
onInstall = {
@@ -102,6 +104,8 @@ fun PatchOptionsScreenContent(
patchLockState: (KnownPatch) -> PatchLock,
variantIndex: (KnownPatch) -> Int,
onSelectVariant: (KnownPatch, Int) -> Unit,
isSubOptionEnabled: (KnownPatch, PatchSubOption) -> Boolean,
onToggleSubOption: (KnownPatch, PatchSubOption, Boolean) -> Unit,
isConfigValid: Boolean,
onInstall: () -> Unit,
@@ -171,6 +175,8 @@ fun PatchOptionsScreenContent(
lockState = patchLockState,
variantIndex = variantIndex,
onSelectVariant = onSelectVariant,
isSubOptionEnabled = isSubOptionEnabled,
onToggleSubOption = onToggleSubOption,
modifier = Modifier.padding(top = 4.dp),
)
@@ -24,6 +24,7 @@ import androidx.compose.ui.unit.dp
import com.meowarex.rlmobile.R
import com.meowarex.rlmobile.ui.screens.patchopts.KnownPatch
import com.meowarex.rlmobile.ui.screens.patchopts.PatchLock
import com.meowarex.rlmobile.ui.screens.patchopts.PatchSubOption
private data class LockInfo(val patch: KnownPatch, val lock: PatchLock)
@@ -36,6 +37,8 @@ fun PatchSelectionAccordion(
lockState: (KnownPatch) -> PatchLock,
variantIndex: (KnownPatch) -> Int,
onSelectVariant: (KnownPatch, Int) -> Unit,
isSubOptionEnabled: (KnownPatch, PatchSubOption) -> Boolean,
onToggleSubOption: (KnownPatch, PatchSubOption, Boolean) -> Unit,
modifier: Modifier = Modifier,
) {
var expanded by rememberSaveable { mutableStateOf(false) }
@@ -131,6 +134,29 @@ fun PatchSelectionAccordion(
}
}
}
if (patch.subOptions.isNotEmpty()) {
AnimatedVisibility(visible = checked) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(start = 12.dp, end = 4.dp, top = 4.dp, bottom = 4.dp),
) {
for (subOption in patch.subOptions) key(subOption) {
PatchSwitchRow(
title = stringResource(subOption.titleRes),
description = stringResource(subOption.descRes),
checked = isSubOptionEnabled(patch, subOption),
lock = PatchLock.Free,
onCheckedChange = {
onToggleSubOption(patch, subOption, it)
},
onLockedTap = {},
)
}
}
}
}
}
}
}
@@ -289,6 +289,10 @@
<string name="patch_mini_player_redesign_title">Redesigned Mini-Player</string>
<string name="patch_mini_player_redesign_desc">Integrates the Seekbar into the control panel at the bottom of the screen.</string>
<string name="patch_mini_player_gestures_title">Mini-Player Gestures</string>
<string name="patch_mini_player_gestures_desc">Swipe up on the mini-player to open the full player.</string>
<string name="patch_mini_player_left_right_gestures_title">Next/Previous Gestures</string>
<string name="patch_mini_player_left_right_gestures_desc">Swipe left or right on the mini-player to skip tracks.</string>
<string name="patch_mini_player_variant_floating_title">Floating</string>
<string name="patch_mini_player_variant_square_grey_title">Grey</string>
<string name="patch_mini_player_variant_square_black_title">Black</string>