Improve Swipe Gstures <3

This commit is contained in:
2026-06-26 22:38:04 +10:00
parent 22526f4e0b
commit 04ea66acdb
7 changed files with 156 additions and 40 deletions
@@ -148,15 +148,15 @@ enum class KnownPatch(
default = Disabled,
defaultVariantIndex = 2,
variants = listOf(
PatchVariant( // 0: Floating — stock rounded pill (no patch; the progress border is an option)
PatchVariant( // 0: Floating — stock rounded pill
titleRes = R.string.patch_mini_player_variant_floating_title,
fileNames = emptyList(),
),
PatchVariant( // 1: Grey — square, theme background (shown as "Legacy" when Dynamic BG is on)
PatchVariant( // 1: Grey — square
titleRes = R.string.patch_mini_player_variant_square_grey_title,
fileNames = listOf("mini-player-grey.patch"),
),
PatchVariant( // 2: Black — square, forced black background (hidden when Dynamic BG is on)
PatchVariant( // 2: Black — square black background
titleRes = R.string.patch_mini_player_variant_square_black_title,
fileNames = listOf("mini-player-black.patch"),
),
@@ -199,11 +199,15 @@ enum class KnownPatch(
"radiant/MiniPlayerGestures\$ApplyPending.smali",
),
),
PatchOption.Toggle(
PatchOption.Choice(
key = "swipe_up_drag",
titleRes = R.string.patch_mini_player_drag_title,
descRes = R.string.patch_mini_player_drag_desc,
default = true,
entries = listOf(
ChoiceEntry(R.string.patch_mini_player_drag_static, value = "0x0"),
ChoiceEntry(R.string.patch_mini_player_drag_drag, value = "0x1"),
),
defaultIndex = 1,
requiresOption = "gestures",
token = "RL_MINI_PLAYER_SWIPE_UP_DRAG",
),
@@ -62,9 +62,12 @@ sealed interface PatchOption {
@StringRes override val descRes: Int,
val entries: List<ChoiceEntry>,
val defaultIndex: Int = 0,
val requiresOption: String? = null,
val token: String? = null,
) : PatchOption
}
data class ChoiceEntry(
@StringRes val labelRes: Int,
val value: String? = null,
)
@@ -52,6 +52,10 @@ data class PatchOptions(
fun sliderValue(spec: PatchSpec, option: OptionSpec.Slider): Float =
(optionFloats["${spec.id}/${option.key}"] ?: option.default).coerceIn(option.min, option.max)
fun choiceIndex(spec: PatchSpec, option: OptionSpec.Choice): Int =
(optionInts["${spec.id}/${option.key}"] ?: option.defaultIndex)
.coerceIn(0, option.entries.lastIndex.coerceAtLeast(0))
fun isToggleOn(spec: PatchSpec, option: OptionSpec.Toggle): Boolean =
optionBools["${spec.id}/${option.key}"] ?: option.default
@@ -121,7 +125,16 @@ data class PatchOptions(
val encode = option.encode ?: continue
put("__${token}__", encode.encode(sliderValue(spec, option)))
}
is OptionSpec.Choice -> Unit
is OptionSpec.Choice -> {
val token = option.token ?: continue
val gatedOff = option.requiresOption?.let { key ->
spec.advancedOptions.filterIsInstance<OptionSpec.Toggle>()
.firstOrNull { it.key == key }
?.let { !isToggleOn(spec, it) }
} ?: false
val index = if (gatedOff) 0 else choiceIndex(spec, option)
put("__${token}__", option.values.getOrNull(index) ?: continue)
}
}
}
}
@@ -98,10 +98,13 @@ sealed interface OptionSpec {
@SerialName("choice")
data class Choice(
override val key: String,
override val title: String,
override val title: String = "",
override val description: String = "",
val entries: List<String> = emptyList(),
val defaultIndex: Int = 0,
val values: List<String> = emptyList(),
val requiresOption: String? = null,
val token: String? = null,
) : OptionSpec
}
@@ -187,6 +190,9 @@ private fun PatchOption.toSpec(resolve: (Int) -> String): OptionSpec = when (thi
description = resolve(descRes),
entries = entries.map { resolve(it.labelRes) },
defaultIndex = defaultIndex,
values = entries.map { it.value ?: "" },
requiresOption = requiresOption,
token = token,
)
}
@@ -1,5 +1,8 @@
package com.meowarex.rlmobile.ui.screens.patchopts.components
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
@@ -11,6 +14,7 @@ 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.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
@@ -87,18 +91,91 @@ fun PatchAdvancedOptionsSheet(
HorizontalDivider()
val sheetOptions = patch.advancedOptions.filter { it !is OptionSpec.Toggle || !it.inline }
val parentKeys = patch.advancedOptions
.filterIsInstance<OptionSpec.Toggle>()
.filter { !it.inline }
.map { it.key }
.toSet()
val sheetOptions = patch.advancedOptions.filter { option ->
when {
option is OptionSpec.Toggle && option.inline -> false
option is OptionSpec.Toggle && option.requiresOption in parentKeys -> false
else -> true
}
}
for (option in sheetOptions) key(option.key) {
val lock = patch.optionLock(option, selectedVariant) { state.toggle(patch, it) }
when (option) {
is OptionSpec.Toggle -> ToggleOptionRow(
title = option.title,
description = option.description,
checked = state.toggle(patch, option),
onCheckedChange = { state.setToggle(patch, option, it) },
locked = lock != OptionLock.Free,
onLockedTap = { lockDialog = lock },
)
is OptionSpec.Toggle -> {
val toggleOn = state.toggle(patch, option)
val gatedChoices = patch.advancedOptions.filterIsInstance<OptionSpec.Choice>()
.filter { it.requiresOption == option.key }
val gatedToggles = patch.advancedOptions.filterIsInstance<OptionSpec.Toggle>()
.filter { it.requiresOption == option.key && !it.inline }
val hasSubOptions = gatedChoices.isNotEmpty() || gatedToggles.isNotEmpty()
val carded = hasSubOptions && toggleOn
Column(
modifier = if (carded) {
Modifier
.fillMaxWidth()
.clip(MaterialTheme.shapes.medium)
.background(MaterialTheme.colorScheme.primary.copy(alpha = 0.07f))
.border(
width = 1.5.dp,
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.55f),
shape = MaterialTheme.shapes.medium,
)
.padding(horizontal = 12.dp, vertical = 4.dp)
} else {
Modifier.fillMaxWidth()
},
) {
ToggleOptionRow(
title = option.title,
description = option.description,
checked = toggleOn,
onCheckedChange = { state.setToggle(patch, option, it) },
locked = lock != OptionLock.Free,
onLockedTap = { lockDialog = lock },
modifier = if (hasSubOptions) Modifier.padding(vertical = 6.dp) else Modifier,
)
if (hasSubOptions) {
AnimatedVisibility(visible = toggleOn) {
Column(
verticalArrangement = Arrangement.spacedBy(10.dp),
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 8.dp),
) {
gatedChoices.forEach { choice ->
key(choice.key) {
ChoiceOptionRow(
title = choice.title,
entries = choice.entries,
selectedIndex = state.choice(patch, choice),
onSelect = { state.setChoice(patch, choice, it) },
)
}
}
gatedToggles.forEach { sub ->
key(sub.key) {
val subLock = patch.optionLock(sub, selectedVariant) { state.toggle(patch, it) }
ToggleOptionRow(
title = sub.title,
description = sub.description,
checked = state.toggle(patch, sub),
onCheckedChange = { state.setToggle(patch, sub, it) },
locked = subLock != OptionLock.Free,
onLockedTap = { lockDialog = subLock },
)
}
}
}
}
}
}
}
is OptionSpec.Slider -> SliderOptionRow(
title = option.title,
@@ -110,13 +187,15 @@ fun PatchAdvancedOptionsSheet(
onValueChange = { state.setSlider(patch, option, it) },
)
is OptionSpec.Choice -> ChoiceOptionRow(
title = option.title,
description = option.description,
entries = option.entries,
selectedIndex = state.choice(patch, option),
onSelect = { state.setChoice(patch, option, it) },
)
is OptionSpec.Choice ->
if (option.requiresOption == null) {
ChoiceOptionRow(
title = option.title,
entries = option.entries,
selectedIndex = state.choice(patch, option),
onSelect = { state.setChoice(patch, option, it) },
)
}
}
}
@@ -147,13 +226,14 @@ private fun ToggleOptionRow(
onCheckedChange: (Boolean) -> Unit,
locked: Boolean = false,
onLockedTap: () -> Unit = {},
modifier: Modifier = Modifier,
) {
val interactionSource = remember(::MutableInteractionSource)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
modifier = Modifier
modifier = modifier
.fillMaxWidth()
.clickable(
interactionSource = interactionSource,
@@ -279,18 +359,25 @@ private fun snapToNearestStep(
@Composable
private fun ChoiceOptionRow(
title: String,
description: String,
entries: List<String>,
selectedIndex: Int,
onSelect: (Int) -> Unit,
title: String = "",
modifier: Modifier = Modifier,
) {
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(6.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
OptionText(title = title, description = description)
if (title.isNotEmpty()) {
Text(
text = title,
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
)
}
SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) {
entries.forEachIndexed { index, entry ->
SegmentedButton(
+4 -2
View File
@@ -295,8 +295,10 @@
<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_drag_title">Enable Drag</string>
<string name="patch_mini_player_drag_desc">Makes player follow your finger when swiping up.</string>
<string name="patch_mini_player_drag_title">Swipe Up Style</string>
<string name="patch_mini_player_drag_desc">Whether the player follows your finger when swiping up to open it.</string>
<string name="patch_mini_player_drag_static">Static</string>
<string name="patch_mini_player_drag_drag">Drag</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_dynamic_bg_title">Mini-Player Dynamic Background</string>
+9 -8
View File
@@ -35,14 +35,14 @@
"order": 30,
"fileNames": ["player-backdrop.patch"],
"title": "Player Backdrop",
"description": "Restores the legacy translucent backdrop blur behind the player.",
"description": "Restores the legacy translucent backdrop blur behind the player",
"default": true,
"advancedOptions": [
{
"type": "slider",
"key": "blur_strength",
"title": "Blur Strength",
"description": "How strongly the album art behind the player is blurred. 0% is sharp, 50% matches the original look.",
"description": "How strongly the album art behind the player is blurred",
"default": 50,
"min": 0,
"max": 100,
@@ -55,7 +55,7 @@
"type": "slider",
"key": "dimming",
"title": "Backdrop Dimming",
"description": "Darkens the backdrop so the foreground controls stay legible.",
"description": "Darkens the backdrop so the foreground controls stay legible",
"default": 50,
"min": 0,
"max": 100,
@@ -163,7 +163,7 @@
"type": "toggle",
"key": "gestures",
"title": "Mini-Player Gestures",
"description": "Swipe up on the mini-player to open the full player.",
"description": "Adds swipe up gesture to the mini-player to reopen the player",
"default": true,
"fileNames": ["mini-player-gestures.patch"],
"extensionFiles": [
@@ -176,11 +176,12 @@
]
},
{
"type": "toggle",
"type": "choice",
"key": "swipe_up_drag",
"title": "Enable Drag",
"description": "Makes player follow your finger when swiping up.",
"default": true,
"title": "Swipe Up Style",
"entries": ["Static", "Drag"],
"values": ["0x0", "0x1"],
"defaultIndex": 1,
"requiresOption": "gestures",
"token": "RL_MINI_PLAYER_SWIPE_UP_DRAG"
},