diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/KnownPatch.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/KnownPatch.kt index eb2847b..38d7bbe 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/KnownPatch.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/KnownPatch.kt @@ -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", ), diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOption.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOption.kt index b8d24bf..4ce2e8e 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOption.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOption.kt @@ -62,9 +62,12 @@ sealed interface PatchOption { @StringRes override val descRes: Int, val entries: List, val defaultIndex: Int = 0, + val requiresOption: String? = null, + val token: String? = null, ) : PatchOption } data class ChoiceEntry( @StringRes val labelRes: Int, + val value: String? = null, ) diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOptions.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOptions.kt index f9069b9..96a1292 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOptions.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOptions.kt @@ -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() + .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) + } } } } diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchSpec.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchSpec.kt index 223b8c2..22aa3d7 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchSpec.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchSpec.kt @@ -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 = emptyList(), val defaultIndex: Int = 0, + val values: List = 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, ) } diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/PatchAdvancedOptionsSheet.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/PatchAdvancedOptionsSheet.kt index b6a0e74..6c1b8cf 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/PatchAdvancedOptionsSheet.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/PatchAdvancedOptionsSheet.kt @@ -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() + .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() + .filter { it.requiresOption == option.key } + val gatedToggles = patch.advancedOptions.filterIsInstance() + .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, 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( diff --git a/Manager/app/src/main/res/values/strings.xml b/Manager/app/src/main/res/values/strings.xml index 6e3e01e..2aaa020 100644 --- a/Manager/app/src/main/res/values/strings.xml +++ b/Manager/app/src/main/res/values/strings.xml @@ -295,8 +295,10 @@ Integrates the Seekbar into the control panel at the bottom of the screen. Mini-Player Gestures Swipe up on the mini-player to open the full player. - Enable Drag - Makes player follow your finger when swiping up. + Swipe Up Style + Whether the player follows your finger when swiping up to open it. + Static + Drag Next/Previous Gestures Swipe left or right on the mini-player to skip tracks. Mini-Player Dynamic Background diff --git a/patches/manifest.json b/patches/manifest.json index d00fa61..cc2a095 100644 --- a/patches/manifest.json +++ b/patches/manifest.json @@ -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" },