mirror of
https://github.com/meowarex/rl-mobile.git
synced 2026-10-15 07:53:12 +11:00
feat(swipe to queue): add to playlist action + both directions for queue track removal + options UI rework
This commit is contained in:
1 parent
2cb51576df
commit
49693ca36b
36 files changed
+953
-151
No files matched your search
@@ -471,25 +471,27 @@
|
||||
"radiant/swipe/SuggestionsResolver.smali"
|
||||
],
|
||||
"title": "Swipe to Queue",
|
||||
"description": "Swipe on tracks/albums/playlists to add them to the play queue",
|
||||
"description": "Swipe on tracks/albums/playlists to add them to the play queue (and more actions)",
|
||||
"default": false,
|
||||
"advancedOptions": [
|
||||
{
|
||||
"type": "choice",
|
||||
"key": "left_action",
|
||||
"title": "Left Swipe Action",
|
||||
"description": "Choose the action for a left swipe",
|
||||
"entries": [
|
||||
"Disabled",
|
||||
"Play next",
|
||||
"Add to queue"
|
||||
"Add to queue",
|
||||
"Add to playlist"
|
||||
],
|
||||
"values": [
|
||||
"0x0",
|
||||
"0x1",
|
||||
"0x2"
|
||||
"0x2",
|
||||
"0x3"
|
||||
],
|
||||
"defaultIndex": 2,
|
||||
"inline": true,
|
||||
"distinctFrom": "right_action",
|
||||
"token": "RL_SWIPE_TO_QUEUE_LEFT_ACTION"
|
||||
},
|
||||
@@ -497,18 +499,20 @@
|
||||
"type": "choice",
|
||||
"key": "right_action",
|
||||
"title": "Right Swipe Action",
|
||||
"description": "Choose the action for a right swipe",
|
||||
"entries": [
|
||||
"Disabled",
|
||||
"Play next",
|
||||
"Add to queue"
|
||||
"Add to queue",
|
||||
"Add to playlist"
|
||||
],
|
||||
"values": [
|
||||
"0x0",
|
||||
"0x1",
|
||||
"0x2"
|
||||
"0x2",
|
||||
"0x3"
|
||||
],
|
||||
"defaultIndex": 0,
|
||||
"inline": true,
|
||||
"distinctFrom": "left_action",
|
||||
"token": "RL_SWIPE_TO_QUEUE_RIGHT_ACTION"
|
||||
},
|
||||
@@ -516,11 +520,10 @@
|
||||
"type": "choice",
|
||||
"key": "remove_direction",
|
||||
"title": "Remove Track from Queue",
|
||||
"description": "Choose the swipe direction used to remove items from the play queue",
|
||||
"entries": [
|
||||
"Left swipe",
|
||||
"Right swipe",
|
||||
"Both"
|
||||
"Both directions"
|
||||
],
|
||||
"values": [
|
||||
"0x4",
|
||||
@@ -528,6 +531,8 @@
|
||||
"0xc"
|
||||
],
|
||||
"defaultIndex": 0,
|
||||
"forceDropdown": true,
|
||||
"inline": true,
|
||||
"token": "RL_SWIPE_TO_QUEUE_REMOVE_DIRECTION"
|
||||
},
|
||||
{
|
||||
@@ -543,6 +548,13 @@
|
||||
"title": "Play Next Color",
|
||||
"default": -14408663,
|
||||
"token": "RL_SWIPE_TO_QUEUE_PLAY_NEXT_COLOR"
|
||||
},
|
||||
{
|
||||
"type": "color",
|
||||
"key": "add_to_playlist_color",
|
||||
"title": "Add to Playlist Color",
|
||||
"default": -14408663,
|
||||
"token": "RL_SWIPE_TO_QUEUE_ADD_TO_PLAYLIST_COLOR"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
+168
@@ -3,26 +3,49 @@ package com.meowarex.rlmobile.ui.components.radiant
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||
import androidx.compose.animation.core.MutableTransitionState
|
||||
import androidx.compose.animation.core.animateFloat
|
||||
import androidx.compose.animation.core.rememberTransition
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
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.graphics.Color
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.semantics.role
|
||||
import androidx.compose.ui.semantics.selected
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Popup
|
||||
import androidx.compose.ui.window.PopupProperties
|
||||
import com.meowarex.rlmobile.R
|
||||
import com.meowarex.rlmobile.ui.theme.*
|
||||
|
||||
/**
|
||||
@@ -113,6 +136,151 @@ fun RadiantTextField(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dropdown
|
||||
*/
|
||||
@Composable
|
||||
fun RadiantDropdown(
|
||||
options: List<String>,
|
||||
selectedIndex: Int,
|
||||
onSelect: (Int) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
disabledIndices: Set<Int> = emptySet(),
|
||||
) {
|
||||
var expanded by rememberSaveable { mutableStateOf(false) }
|
||||
val visibilityState = remember { MutableTransitionState(false) }
|
||||
visibilityState.targetState = expanded
|
||||
val transition = rememberTransition(visibilityState, label = "Dropdown")
|
||||
val progress by transition.animateFloat(
|
||||
transitionSpec = { tween(durationMillis = 160, easing = FastOutSlowInEasing) },
|
||||
label = "DropdownProgress",
|
||||
) {
|
||||
if (it) 1f else 0f
|
||||
}
|
||||
val scheme = MaterialTheme.colorScheme
|
||||
val selectedLabel = options.getOrNull(selectedIndex).orEmpty()
|
||||
val shape = if (radiantStyle.native) MaterialTheme.shapes.extraSmall else RoundedCornerShape(16.dp)
|
||||
var anchorWidthPx by remember { mutableIntStateOf(0) }
|
||||
val anchorWidth = with(LocalDensity.current) { anchorWidthPx.toDp() }
|
||||
val positionProvider = MenuDefaults.rememberDropdownMenuPopupPositionProvider(
|
||||
dropdownMenuAnchorPosition = MenuAnchorPosition.Below,
|
||||
offset = DpOffset(0.dp, 6.dp),
|
||||
)
|
||||
Box(modifier = modifier.fillMaxWidth()) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.onSizeChanged { anchorWidthPx = it.width }
|
||||
.background(scheme.surfaceContainerLowest.copy(alpha = 0.8f), shape)
|
||||
.then(
|
||||
if (radiantStyle.native) Modifier.border(1.dp, scheme.outline, shape)
|
||||
else Modifier.border(
|
||||
RadiantDesign.Hairline,
|
||||
hairlineBrush(scheme.onSurface, RadiantDesign.BORDER_ALPHA * 1.5f),
|
||||
shape,
|
||||
)
|
||||
)
|
||||
.clip(shape)
|
||||
.clickable(enabled = enabled, role = Role.Button) { expanded = !expanded }
|
||||
.alpha(if (enabled) 1f else 0.45f)
|
||||
.heightIn(min = 52.dp)
|
||||
.padding(horizontal = 16.dp, vertical = 10.dp),
|
||||
) {
|
||||
Text(
|
||||
text = selectedLabel,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = scheme.onSurface,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_arrow_down_small),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.rotate(180f * progress),
|
||||
)
|
||||
}
|
||||
|
||||
if (
|
||||
anchorWidthPx > 0 &&
|
||||
(visibilityState.currentState || visibilityState.targetState || transition.isRunning)
|
||||
) {
|
||||
Popup(
|
||||
popupPositionProvider = positionProvider,
|
||||
onDismissRequest = { expanded = false },
|
||||
properties = PopupProperties(focusable = true),
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.width(anchorWidth)
|
||||
.graphicsLayer {
|
||||
alpha = progress
|
||||
scaleX = 0.92f + 0.08f * progress
|
||||
scaleY = 0.92f + 0.08f * progress
|
||||
transformOrigin = positionProvider.transformOrigin
|
||||
},
|
||||
shape = shape,
|
||||
color = scheme.surfaceContainer,
|
||||
tonalElevation = 0.dp,
|
||||
shadowElevation = 8.dp,
|
||||
border = BorderStroke(
|
||||
RadiantDesign.Hairline,
|
||||
scheme.onSurface.copy(alpha = RadiantDesign.BORDER_ALPHA * 2f),
|
||||
),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.heightIn(max = 320.dp)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(vertical = 6.dp),
|
||||
) {
|
||||
options.forEachIndexed { index, option ->
|
||||
val selected = index == selectedIndex
|
||||
val itemEnabled = enabled && index !in disabledIndices
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(
|
||||
text = option,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
},
|
||||
trailingIcon = {
|
||||
RadiantRadio(
|
||||
selected = selected,
|
||||
onClick = null,
|
||||
enabled = itemEnabled,
|
||||
)
|
||||
},
|
||||
enabled = itemEnabled,
|
||||
onClick = {
|
||||
onSelect(index)
|
||||
expanded = false
|
||||
},
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 6.dp, vertical = 2.dp)
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.semantics {
|
||||
this.selected = selected
|
||||
role = Role.RadioButton
|
||||
}
|
||||
.then(
|
||||
if (selected) Modifier.background(scheme.primary.copy(alpha = 0.1f))
|
||||
else Modifier
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Slider. (i prefer the old on tbh so i reverted it back <3)
|
||||
*/
|
||||
|
||||
+23
-38
@@ -33,11 +33,13 @@ import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.meowarex.rlmobile.R
|
||||
import com.meowarex.rlmobile.ui.components.radiant.RadiantDropdown
|
||||
import com.meowarex.rlmobile.ui.legacy.components.ResetToDefaultButton
|
||||
import com.meowarex.rlmobile.ui.screens.patchopts.OptionLock
|
||||
import com.meowarex.rlmobile.ui.screens.patchopts.OptionSpec
|
||||
import com.meowarex.rlmobile.ui.screens.patchopts.PatchOptionState
|
||||
import com.meowarex.rlmobile.ui.screens.patchopts.hiddenForVariant
|
||||
import com.meowarex.rlmobile.ui.screens.patchopts.isInline
|
||||
import com.meowarex.rlmobile.ui.screens.patchopts.optionLock
|
||||
import com.meowarex.rlmobile.ui.screens.patchopts.PatchSpec
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -103,12 +105,12 @@ fun PatchAdvancedOptionsSheet(
|
||||
|
||||
val parentKeys = patch.advancedOptions
|
||||
.filterIsInstance<OptionSpec.Toggle>()
|
||||
.filter { !it.inline }
|
||||
.filter { !it.isInline }
|
||||
.map { it.key }
|
||||
.toSet()
|
||||
val sheetOptions = patch.advancedOptions.filter { option ->
|
||||
when {
|
||||
option is OptionSpec.Toggle && option.inline -> false
|
||||
option.isInline -> false
|
||||
option is OptionSpec.Toggle && option.requiresOption in parentKeys -> false
|
||||
// Options belonging to another variant are hidden, not greyed out.
|
||||
option.hiddenForVariant(selectedVariant) -> false
|
||||
@@ -121,7 +123,7 @@ fun PatchAdvancedOptionsSheet(
|
||||
is OptionSpec.Toggle -> {
|
||||
val toggleOn = state.toggle(patch, option)
|
||||
val gatedChoices = patch.advancedOptions.filterIsInstance<OptionSpec.Choice>()
|
||||
.filter { it.requiresOption == option.key }
|
||||
.filter { it.requiresOption == option.key && !it.isInline }
|
||||
val gatedToggles = patch.advancedOptions.filterIsInstance<OptionSpec.Toggle>()
|
||||
.filter { it.requiresOption == option.key && !it.inline }
|
||||
val hasSubOptions = gatedChoices.isNotEmpty() || gatedToggles.isNotEmpty()
|
||||
@@ -164,6 +166,7 @@ fun PatchAdvancedOptionsSheet(
|
||||
key(choice.key) {
|
||||
ChoiceOptionRow(
|
||||
title = choice.title,
|
||||
description = choice.description,
|
||||
entries = choice.entries,
|
||||
selectedIndex = state.choice(patch, choice),
|
||||
forceDropdown = choice.forceDropdown,
|
||||
@@ -208,6 +211,7 @@ fun PatchAdvancedOptionsSheet(
|
||||
if (option.requiresOption == null) {
|
||||
ChoiceOptionRow(
|
||||
title = option.title,
|
||||
description = option.description,
|
||||
entries = option.entries,
|
||||
selectedIndex = state.choice(patch, option),
|
||||
forceDropdown = option.forceDropdown,
|
||||
@@ -387,11 +391,12 @@ private fun snapToNearestStep(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChoiceOptionRow(
|
||||
internal fun ChoiceOptionRow(
|
||||
entries: List<String>,
|
||||
selectedIndex: Int,
|
||||
onSelect: (Int) -> Unit,
|
||||
title: String = "",
|
||||
description: String = "",
|
||||
forceDropdown: Boolean = false,
|
||||
disabledIndices: Set<Int> = emptySet(),
|
||||
modifier: Modifier = Modifier,
|
||||
@@ -399,48 +404,28 @@ private fun ChoiceOptionRow(
|
||||
Column(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
horizontalAlignment = Alignment.Start,
|
||||
) {
|
||||
if (title.isNotEmpty()) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
)
|
||||
}
|
||||
if (description.isNotEmpty()) {
|
||||
Text(
|
||||
text = description,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
if (forceDropdown || entries.size > 3) {
|
||||
var expanded by rememberSaveable { mutableStateOf(false) }
|
||||
val selectedLabel = entries.getOrNull(selectedIndex).orEmpty()
|
||||
|
||||
Box(modifier = Modifier.fillMaxWidth()) {
|
||||
OutlinedButton(
|
||||
onClick = { expanded = true },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text(
|
||||
text = selectedLabel,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
DropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = { expanded = false },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
entries.forEachIndexed { index, entry ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(entry) },
|
||||
enabled = index !in disabledIndices,
|
||||
onClick = {
|
||||
onSelect(index)
|
||||
expanded = false
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
RadiantDropdown(
|
||||
options = entries,
|
||||
selectedIndex = selectedIndex,
|
||||
onSelect = onSelect,
|
||||
disabledIndices = disabledIndices,
|
||||
)
|
||||
return@Column
|
||||
}
|
||||
SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) {
|
||||
|
||||
+40
-15
@@ -36,6 +36,8 @@ import com.meowarex.rlmobile.ui.screens.patchopts.PatchOptions
|
||||
import com.meowarex.rlmobile.ui.screens.patchopts.PatchSpec
|
||||
import com.meowarex.rlmobile.ui.screens.patchopts.effectiveVariants
|
||||
import com.meowarex.rlmobile.ui.screens.patchopts.resolveVariantIndex
|
||||
import com.meowarex.rlmobile.ui.screens.patchopts.conflictingChoices
|
||||
import com.meowarex.rlmobile.ui.screens.patchopts.isInline
|
||||
|
||||
private data class LockInfo(val patch: PatchSpec, val lock: PatchLock)
|
||||
|
||||
@@ -131,11 +133,9 @@ fun PatchSelectionAccordion(
|
||||
val checked = isEnabled(patch)
|
||||
val lock = lockState(patch)
|
||||
|
||||
val inlineToggles = patch.advancedOptions
|
||||
.filterIsInstance<OptionSpec.Toggle>()
|
||||
.filter { it.inline }
|
||||
val hasSheetOptions = patch.advancedOptions.any { it !is OptionSpec.Toggle || !it.inline }
|
||||
val hasSettings = patch.variants.isNotEmpty() || inlineToggles.isNotEmpty() || hasSheetOptions
|
||||
val inlineOptions = patch.advancedOptions.filter { it.isInline }
|
||||
val hasSheetOptions = patch.advancedOptions.any { !it.isInline }
|
||||
val hasSettings = patch.variants.isNotEmpty() || inlineOptions.isNotEmpty() || hasSheetOptions
|
||||
|
||||
// When a patch is enabled and has settings show a carded view of the settings
|
||||
val carded = checked && hasSettings
|
||||
@@ -191,16 +191,41 @@ fun PatchSelectionAccordion(
|
||||
)
|
||||
}
|
||||
|
||||
for (option in inlineToggles) key(option.key) {
|
||||
val show = option.requiresVariant == null ||
|
||||
option.requiresVariant == resolvedVariant
|
||||
if (show) {
|
||||
InlineToggleRow(
|
||||
title = option.title,
|
||||
description = option.description,
|
||||
checked = optionState.toggle(patch, option),
|
||||
onCheckedChange = { optionState.setToggle(patch, option, it) },
|
||||
)
|
||||
for (option in inlineOptions) key(option.key) {
|
||||
when (option) {
|
||||
is OptionSpec.Toggle -> {
|
||||
val show = option.requiresVariant == null ||
|
||||
option.requiresVariant == resolvedVariant
|
||||
if (show) {
|
||||
InlineToggleRow(
|
||||
title = option.title,
|
||||
description = option.description,
|
||||
checked = optionState.toggle(patch, option),
|
||||
onCheckedChange = { optionState.setToggle(patch, option, it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
is OptionSpec.Choice -> {
|
||||
val requiredToggle = option.requiresOption?.let { key ->
|
||||
patch.advancedOptions.filterIsInstance<OptionSpec.Toggle>()
|
||||
.firstOrNull { it.key == key }
|
||||
}
|
||||
if (requiredToggle == null || optionState.toggle(patch, requiredToggle)) {
|
||||
ChoiceOptionRow(
|
||||
title = option.title,
|
||||
description = option.description,
|
||||
entries = option.entries,
|
||||
selectedIndex = optionState.choice(patch, option),
|
||||
forceDropdown = option.forceDropdown,
|
||||
disabledIndices = patch.conflictingChoices(option)
|
||||
.map { optionState.choice(patch, it) }
|
||||
.filter { it != 0 }
|
||||
.toSet(),
|
||||
onSelect = { optionState.setChoice(patch, option, it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+11
-7
@@ -195,10 +195,9 @@ class PatchOptionsModel(
|
||||
}
|
||||
|
||||
fun isAdvancedModified(spec: PatchSpec): Boolean = spec.advancedOptions.any { option ->
|
||||
if (option.isInline) return@any false
|
||||
when (option) {
|
||||
// inline toggles sit next to the variant picker, not in the sheet, so they
|
||||
// must not light up the sheet's "modified" dot
|
||||
is OptionSpec.Toggle -> !option.inline && toggleValue(spec, option) != option.default
|
||||
is OptionSpec.Toggle -> toggleValue(spec, option) != option.default
|
||||
is OptionSpec.Slider -> sliderValue(spec, option) != option.default
|
||||
is OptionSpec.Choice -> choiceValue(spec, option) != option.defaultIndex
|
||||
is OptionSpec.Color -> colorValue(spec, option) != option.default
|
||||
@@ -206,12 +205,17 @@ class PatchOptionsModel(
|
||||
}
|
||||
|
||||
fun resetAdvanced(spec: PatchSpec) {
|
||||
val prefix = "${spec.id}/"
|
||||
optionBools = optionBools.filterKeys { !it.startsWith(prefix) }
|
||||
optionFloats = optionFloats.filterKeys { !it.startsWith(prefix) }
|
||||
val advancedOptions = spec.advancedOptions.filter { !it.isInline }
|
||||
val boolKeys = advancedOptions.filterIsInstance<OptionSpec.Toggle>().map { keyOf(spec, it) }.toSet()
|
||||
val floatKeys = advancedOptions.filterIsInstance<OptionSpec.Slider>().map { keyOf(spec, it) }.toSet()
|
||||
val intKeys = advancedOptions.filter { it is OptionSpec.Choice || it is OptionSpec.Color }
|
||||
.map { keyOf(spec, it) }
|
||||
.toSet()
|
||||
optionBools = optionBools.filterKeys { it !in boolKeys }
|
||||
optionFloats = optionFloats.filterKeys { it !in floatKeys }
|
||||
optionInts = normalizeChoiceSelections(
|
||||
specs = listOf(spec),
|
||||
values = optionInts.filterKeys { !it.startsWith(prefix) },
|
||||
values = optionInts.filterKeys { it !in intKeys },
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -126,6 +126,8 @@ sealed interface OptionSpec {
|
||||
val values: List<String> = emptyList(),
|
||||
val requiresOption: String? = null,
|
||||
val forceDropdown: Boolean = false,
|
||||
/** Render inline in the patch card instead of inside the advanced sheet */
|
||||
val inline: Boolean = false,
|
||||
val distinctFrom: String? = null,
|
||||
val token: String? = null,
|
||||
) : OptionSpec
|
||||
@@ -143,6 +145,13 @@ sealed interface OptionSpec {
|
||||
) : OptionSpec
|
||||
}
|
||||
|
||||
val OptionSpec.isInline: Boolean
|
||||
get() = when (this) {
|
||||
is OptionSpec.Toggle -> inline
|
||||
is OptionSpec.Choice -> inline
|
||||
is OptionSpec.Slider, is OptionSpec.Color -> false
|
||||
}
|
||||
|
||||
@Serializable
|
||||
enum class EncodeKind {
|
||||
@SerialName("floatBits")
|
||||
|
||||
+23
-34
@@ -32,6 +32,7 @@ import androidx.compose.ui.unit.dp
|
||||
import com.meowarex.rlmobile.R
|
||||
import com.meowarex.rlmobile.ui.components.radiant.RadiantButton
|
||||
import com.meowarex.rlmobile.ui.components.radiant.RadiantDialog
|
||||
import com.meowarex.rlmobile.ui.components.radiant.RadiantDropdown
|
||||
import com.meowarex.rlmobile.ui.components.radiant.RadiantButtonSize
|
||||
import com.meowarex.rlmobile.ui.components.radiant.RadiantButtonStyle
|
||||
import com.meowarex.rlmobile.ui.components.radiant.RadiantIconButton
|
||||
@@ -45,6 +46,7 @@ import com.meowarex.rlmobile.ui.screens.patchopts.OptionSpec
|
||||
import com.meowarex.rlmobile.ui.screens.patchopts.PatchOptionState
|
||||
import com.meowarex.rlmobile.ui.screens.patchopts.conflictingChoices
|
||||
import com.meowarex.rlmobile.ui.screens.patchopts.hiddenForVariant
|
||||
import com.meowarex.rlmobile.ui.screens.patchopts.isInline
|
||||
import com.meowarex.rlmobile.ui.screens.patchopts.optionLock
|
||||
import com.meowarex.rlmobile.ui.screens.patchopts.PatchSpec
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -110,12 +112,12 @@ fun PatchAdvancedOptionsSheet(
|
||||
|
||||
val parentKeys = patch.advancedOptions
|
||||
.filterIsInstance<OptionSpec.Toggle>()
|
||||
.filter { !it.inline }
|
||||
.filter { !it.isInline }
|
||||
.map { it.key }
|
||||
.toSet()
|
||||
val sheetOptions = patch.advancedOptions.filter { option ->
|
||||
when {
|
||||
option is OptionSpec.Toggle && option.inline -> false
|
||||
option.isInline -> false
|
||||
option is OptionSpec.Toggle && option.requiresOption in parentKeys -> false
|
||||
// Options belonging to another variant are hidden, not greyed out.
|
||||
option.hiddenForVariant(selectedVariant) -> false
|
||||
@@ -128,7 +130,7 @@ fun PatchAdvancedOptionsSheet(
|
||||
is OptionSpec.Toggle -> {
|
||||
val toggleOn = state.toggle(patch, option)
|
||||
val gatedChoices = patch.advancedOptions.filterIsInstance<OptionSpec.Choice>()
|
||||
.filter { it.requiresOption == option.key }
|
||||
.filter { it.requiresOption == option.key && !it.isInline }
|
||||
val gatedToggles = patch.advancedOptions.filterIsInstance<OptionSpec.Toggle>()
|
||||
.filter { it.requiresOption == option.key && !it.inline }
|
||||
val hasSubOptions = gatedChoices.isNotEmpty() || gatedToggles.isNotEmpty()
|
||||
@@ -171,6 +173,7 @@ fun PatchAdvancedOptionsSheet(
|
||||
key(choice.key) {
|
||||
ChoiceOptionRow(
|
||||
title = choice.title,
|
||||
description = choice.description,
|
||||
entries = choice.entries,
|
||||
selectedIndex = state.choice(patch, choice),
|
||||
forceDropdown = choice.forceDropdown,
|
||||
@@ -215,6 +218,7 @@ fun PatchAdvancedOptionsSheet(
|
||||
if (option.requiresOption == null) {
|
||||
ChoiceOptionRow(
|
||||
title = option.title,
|
||||
description = option.description,
|
||||
entries = option.entries,
|
||||
selectedIndex = state.choice(patch, option),
|
||||
forceDropdown = option.forceDropdown,
|
||||
@@ -388,11 +392,12 @@ private fun snapToNearestStep(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChoiceOptionRow(
|
||||
internal fun ChoiceOptionRow(
|
||||
entries: List<String>,
|
||||
selectedIndex: Int,
|
||||
onSelect: (Int) -> Unit,
|
||||
title: String = "",
|
||||
description: String = "",
|
||||
forceDropdown: Boolean = false,
|
||||
disabledIndices: Set<Int> = emptySet(),
|
||||
modifier: Modifier = Modifier,
|
||||
@@ -400,44 +405,28 @@ private fun ChoiceOptionRow(
|
||||
Column(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
horizontalAlignment = Alignment.Start,
|
||||
) {
|
||||
if (title.isNotEmpty()) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
)
|
||||
}
|
||||
if (description.isNotEmpty()) {
|
||||
Text(
|
||||
text = description,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
if (forceDropdown || entries.size > 3) {
|
||||
var expanded by rememberSaveable { mutableStateOf(false) }
|
||||
val selectedLabel = entries.getOrNull(selectedIndex).orEmpty()
|
||||
|
||||
Box(modifier = Modifier.fillMaxWidth()) {
|
||||
RadiantButton(
|
||||
text = selectedLabel,
|
||||
onClick = { expanded = true },
|
||||
style = RadiantButtonStyle.Ghost,
|
||||
fillWidth = true,
|
||||
)
|
||||
DropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = { expanded = false },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
entries.forEachIndexed { index, entry ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(entry) },
|
||||
enabled = index !in disabledIndices,
|
||||
onClick = {
|
||||
onSelect(index)
|
||||
expanded = false
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
RadiantDropdown(
|
||||
options = entries,
|
||||
selectedIndex = selectedIndex,
|
||||
onSelect = onSelect,
|
||||
disabledIndices = disabledIndices,
|
||||
)
|
||||
return@Column
|
||||
}
|
||||
RadiantSegmented(
|
||||
|
||||
+40
-15
@@ -33,6 +33,8 @@ import com.meowarex.rlmobile.ui.components.radiant.RadiantDialog
|
||||
import com.meowarex.rlmobile.ui.theme.radiantSurface
|
||||
import com.meowarex.rlmobile.ui.theme.glassSurface
|
||||
import com.meowarex.rlmobile.ui.screens.patchopts.OptionSpec
|
||||
import com.meowarex.rlmobile.ui.screens.patchopts.conflictingChoices
|
||||
import com.meowarex.rlmobile.ui.screens.patchopts.isInline
|
||||
import com.meowarex.rlmobile.ui.screens.patchopts.PatchLock
|
||||
import com.meowarex.rlmobile.ui.screens.patchopts.PatchOptionState
|
||||
import com.meowarex.rlmobile.ui.screens.patchopts.PatchOptions
|
||||
@@ -134,11 +136,9 @@ fun PatchSelectionAccordion(
|
||||
val checked = isEnabled(patch)
|
||||
val lock = lockState(patch)
|
||||
|
||||
val inlineToggles = patch.advancedOptions
|
||||
.filterIsInstance<OptionSpec.Toggle>()
|
||||
.filter { it.inline }
|
||||
val hasSheetOptions = patch.advancedOptions.any { it !is OptionSpec.Toggle || !it.inline }
|
||||
val hasSettings = patch.variants.isNotEmpty() || inlineToggles.isNotEmpty() || hasSheetOptions
|
||||
val inlineOptions = patch.advancedOptions.filter { it.isInline }
|
||||
val hasSheetOptions = patch.advancedOptions.any { !it.isInline }
|
||||
val hasSettings = patch.variants.isNotEmpty() || inlineOptions.isNotEmpty() || hasSheetOptions
|
||||
|
||||
// When a patch is enabled and has settings show a carded view of the settings
|
||||
val carded = checked && hasSettings
|
||||
@@ -189,16 +189,41 @@ fun PatchSelectionAccordion(
|
||||
)
|
||||
}
|
||||
|
||||
for (option in inlineToggles) key(option.key) {
|
||||
val show = option.requiresVariant == null ||
|
||||
option.requiresVariant == resolvedVariant
|
||||
if (show) {
|
||||
InlineToggleRow(
|
||||
title = option.title,
|
||||
description = option.description,
|
||||
checked = optionState.toggle(patch, option),
|
||||
onCheckedChange = { optionState.setToggle(patch, option, it) },
|
||||
)
|
||||
for (option in inlineOptions) key(option.key) {
|
||||
when (option) {
|
||||
is OptionSpec.Toggle -> {
|
||||
val show = option.requiresVariant == null ||
|
||||
option.requiresVariant == resolvedVariant
|
||||
if (show) {
|
||||
InlineToggleRow(
|
||||
title = option.title,
|
||||
description = option.description,
|
||||
checked = optionState.toggle(patch, option),
|
||||
onCheckedChange = { optionState.setToggle(patch, option, it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
is OptionSpec.Choice -> {
|
||||
val requiredToggle = option.requiresOption?.let { key ->
|
||||
patch.advancedOptions.filterIsInstance<OptionSpec.Toggle>()
|
||||
.firstOrNull { it.key == key }
|
||||
}
|
||||
if (requiredToggle == null || optionState.toggle(patch, requiredToggle)) {
|
||||
ChoiceOptionRow(
|
||||
title = option.title,
|
||||
description = option.description,
|
||||
entries = option.entries,
|
||||
selectedIndex = optionState.choice(patch, option),
|
||||
forceDropdown = option.forceDropdown,
|
||||
disabledIndices = patch.conflictingChoices(option)
|
||||
.map { optionState.choice(patch, it) }
|
||||
.filter { it != 0 }
|
||||
.toSet(),
|
||||
onSelect = { optionState.setChoice(patch, option, it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user