mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-15 10:46:12 -04:00
fix(aap): Fix stem actions volume control, reset button, and Default/No Action semantics
Fix volume up/down doing nothing by using adjustSuggestedStreamVolume instead of dispatchMediaKeyEvent which ignores volume keycodes. Add reset-to-defaults button with confirmation dialog in the stem actions TopAppBar. Rename 'None' to 'Default' (firmware handles the press) and add 'No Action' (claimed but do nothing) to correctly model per-press-type claim mask semantics. Remove disableNone lock; add cross-side auto-set logic so selecting Default resets both sides and selecting an action promotes the other side from Default to No Action.
This commit is contained in:
@@ -53,6 +53,24 @@ class MediaControl @Inject constructor(
|
||||
audioManager.dispatchMediaKeyEvent(KeyEvent(eventTime + 200, eventTime + 200, KeyEvent.ACTION_UP, keyCode, 0))
|
||||
}
|
||||
|
||||
fun adjustVolumeUp() {
|
||||
log(TAG, INFO) { "adjustVolumeUp()" }
|
||||
audioManager.adjustSuggestedStreamVolume(
|
||||
AudioManager.ADJUST_RAISE,
|
||||
AudioManager.USE_DEFAULT_STREAM_TYPE,
|
||||
AudioManager.FLAG_SHOW_UI,
|
||||
)
|
||||
}
|
||||
|
||||
fun adjustVolumeDown() {
|
||||
log(TAG, INFO) { "adjustVolumeDown()" }
|
||||
audioManager.adjustSuggestedStreamVolume(
|
||||
AudioManager.ADJUST_LOWER,
|
||||
AudioManager.USE_DEFAULT_STREAM_TYPE,
|
||||
AudioManager.FLAG_SHOW_UI,
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = logTag("MediaControl")
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.twotone.ArrowBack
|
||||
import androidx.compose.material.icons.twotone.RestartAlt
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExposedDropdownMenuBox
|
||||
@@ -21,6 +23,7 @@ import androidx.compose.material3.ExposedDropdownMenuAnchorType
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -49,6 +52,7 @@ fun StemActionConfigScreenHost(
|
||||
StemActionConfigScreen(
|
||||
state = currentState,
|
||||
onNavigateUp = { vm.navUp() },
|
||||
onReset = { vm.resetAll() },
|
||||
onLeftSingle = { vm.setLeftSingle(it) },
|
||||
onLeftDouble = { vm.setLeftDouble(it) },
|
||||
onLeftTriple = { vm.setLeftTriple(it) },
|
||||
@@ -65,6 +69,7 @@ fun StemActionConfigScreenHost(
|
||||
fun StemActionConfigScreen(
|
||||
state: StemActionConfigViewModel.State,
|
||||
onNavigateUp: () -> Unit,
|
||||
onReset: () -> Unit = {},
|
||||
onLeftSingle: (StemAction) -> Unit = {},
|
||||
onLeftDouble: (StemAction) -> Unit = {},
|
||||
onLeftTriple: (StemAction) -> Unit = {},
|
||||
@@ -74,6 +79,28 @@ fun StemActionConfigScreen(
|
||||
onRightTriple: (StemAction) -> Unit = {},
|
||||
onRightLong: (StemAction) -> Unit = {},
|
||||
) {
|
||||
var showResetDialog by remember { mutableStateOf(false) }
|
||||
|
||||
if (showResetDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showResetDialog = false },
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
onReset()
|
||||
showResetDialog = false
|
||||
}) {
|
||||
Text(stringResource(android.R.string.ok))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { showResetDialog = false }) {
|
||||
Text(stringResource(android.R.string.cancel))
|
||||
}
|
||||
},
|
||||
text = { Text(stringResource(R.string.stem_actions_reset_confirm_message)) },
|
||||
)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
@@ -83,6 +110,14 @@ fun StemActionConfigScreen(
|
||||
Icon(Icons.AutoMirrored.TwoTone.ArrowBack, contentDescription = null)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
IconButton(onClick = { showResetDialog = true }) {
|
||||
Icon(
|
||||
imageVector = Icons.TwoTone.RestartAlt,
|
||||
contentDescription = stringResource(R.string.stem_actions_reset_label),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
) { paddingValues ->
|
||||
@@ -159,10 +194,6 @@ private fun StemActionRow(
|
||||
onLeftChange: (StemAction) -> Unit,
|
||||
onRightChange: (StemAction) -> Unit,
|
||||
) {
|
||||
// Enforce: if one side is non-NONE, the other can't be NONE
|
||||
val leftIsLocked = rightAction != StemAction.NONE
|
||||
val rightIsLocked = leftAction != StemAction.NONE
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -177,7 +208,7 @@ private fun StemActionRow(
|
||||
StemActionDropdown(
|
||||
selected = leftAction,
|
||||
onSelected = onLeftChange,
|
||||
disableNone = leftIsLocked,
|
||||
otherSideAction = rightAction,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
@@ -191,7 +222,7 @@ private fun StemActionRow(
|
||||
StemActionDropdown(
|
||||
selected = rightAction,
|
||||
onSelected = onRightChange,
|
||||
disableNone = rightIsLocked,
|
||||
otherSideAction = leftAction,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
@@ -203,11 +234,17 @@ private fun StemActionRow(
|
||||
private fun StemActionDropdown(
|
||||
selected: StemAction,
|
||||
onSelected: (StemAction) -> Unit,
|
||||
disableNone: Boolean,
|
||||
otherSideAction: StemAction,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
|
||||
val options = if (otherSideAction == StemAction.NONE) {
|
||||
StemAction.entries.filter { it != StemAction.NO_ACTION }
|
||||
} else {
|
||||
StemAction.entries.toList()
|
||||
}
|
||||
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = expanded,
|
||||
onExpandedChange = { expanded = it },
|
||||
@@ -227,23 +264,13 @@ private fun StemActionDropdown(
|
||||
expanded = expanded,
|
||||
onDismissRequest = { expanded = false },
|
||||
) {
|
||||
for (action in StemAction.entries) {
|
||||
val enabled = !(action == StemAction.NONE && disableNone)
|
||||
for (action in options) {
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(
|
||||
text = action.label(),
|
||||
color = if (enabled) MaterialTheme.colorScheme.onSurface
|
||||
else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f),
|
||||
)
|
||||
},
|
||||
text = { Text(action.label()) },
|
||||
onClick = {
|
||||
if (enabled) {
|
||||
onSelected(action)
|
||||
expanded = false
|
||||
}
|
||||
onSelected(action)
|
||||
expanded = false
|
||||
},
|
||||
enabled = enabled,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -253,6 +280,7 @@ private fun StemActionDropdown(
|
||||
@Composable
|
||||
private fun StemAction.label(): String = when (this) {
|
||||
StemAction.NONE -> stringResource(R.string.stem_action_none)
|
||||
StemAction.NO_ACTION -> stringResource(R.string.stem_action_no_action)
|
||||
StemAction.PLAY_PAUSE -> stringResource(R.string.stem_action_play_pause)
|
||||
StemAction.NEXT_TRACK -> stringResource(R.string.stem_action_next_track)
|
||||
StemAction.PREVIOUS_TRACK -> stringResource(R.string.stem_action_previous_track)
|
||||
|
||||
@@ -6,6 +6,7 @@ import eu.darken.capod.common.uix.ViewModel4
|
||||
import eu.darken.capod.reaction.core.stem.StemAction
|
||||
import eu.darken.capod.reaction.core.stem.StemActionSettings
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.first
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
@@ -47,12 +48,60 @@ class StemActionConfigViewModel @Inject constructor(
|
||||
val rightLong: StemAction = StemAction.NONE,
|
||||
)
|
||||
|
||||
fun setLeftSingle(action: StemAction) = launch { stemActionSettings.leftSingle.update { action } }
|
||||
fun setLeftDouble(action: StemAction) = launch { stemActionSettings.leftDouble.update { action } }
|
||||
fun setLeftTriple(action: StemAction) = launch { stemActionSettings.leftTriple.update { action } }
|
||||
fun setLeftLong(action: StemAction) = launch { stemActionSettings.leftLong.update { action } }
|
||||
fun setRightSingle(action: StemAction) = launch { stemActionSettings.rightSingle.update { action } }
|
||||
fun setRightDouble(action: StemAction) = launch { stemActionSettings.rightDouble.update { action } }
|
||||
fun setRightTriple(action: StemAction) = launch { stemActionSettings.rightTriple.update { action } }
|
||||
fun setRightLong(action: StemAction) = launch { stemActionSettings.rightLong.update { action } }
|
||||
fun setLeftSingle(action: StemAction) = launch {
|
||||
stemActionSettings.leftSingle.update { action }
|
||||
applyCrossSideEffect(action, stemActionSettings.rightSingle)
|
||||
}
|
||||
|
||||
fun setLeftDouble(action: StemAction) = launch {
|
||||
stemActionSettings.leftDouble.update { action }
|
||||
applyCrossSideEffect(action, stemActionSettings.rightDouble)
|
||||
}
|
||||
|
||||
fun setLeftTriple(action: StemAction) = launch {
|
||||
stemActionSettings.leftTriple.update { action }
|
||||
applyCrossSideEffect(action, stemActionSettings.rightTriple)
|
||||
}
|
||||
|
||||
fun setLeftLong(action: StemAction) = launch {
|
||||
stemActionSettings.leftLong.update { action }
|
||||
applyCrossSideEffect(action, stemActionSettings.rightLong)
|
||||
}
|
||||
|
||||
fun setRightSingle(action: StemAction) = launch {
|
||||
stemActionSettings.rightSingle.update { action }
|
||||
applyCrossSideEffect(action, stemActionSettings.leftSingle)
|
||||
}
|
||||
|
||||
fun setRightDouble(action: StemAction) = launch {
|
||||
stemActionSettings.rightDouble.update { action }
|
||||
applyCrossSideEffect(action, stemActionSettings.leftDouble)
|
||||
}
|
||||
|
||||
fun setRightTriple(action: StemAction) = launch {
|
||||
stemActionSettings.rightTriple.update { action }
|
||||
applyCrossSideEffect(action, stemActionSettings.leftTriple)
|
||||
}
|
||||
|
||||
fun setRightLong(action: StemAction) = launch {
|
||||
stemActionSettings.rightLong.update { action }
|
||||
applyCrossSideEffect(action, stemActionSettings.leftLong)
|
||||
}
|
||||
|
||||
private suspend fun applyCrossSideEffect(
|
||||
selected: StemAction,
|
||||
otherSide: eu.darken.capod.common.datastore.DataStoreValue<StemAction>,
|
||||
) {
|
||||
when (selected) {
|
||||
StemAction.NONE -> otherSide.update { StemAction.NONE }
|
||||
StemAction.NO_ACTION -> {} // No side-effect
|
||||
else -> {
|
||||
if (otherSide.flow.first() == StemAction.NONE) {
|
||||
otherSide.update { StemAction.NO_ACTION }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun resetAll() = launch { stemActionSettings.resetAll() }
|
||||
}
|
||||
|
||||
@@ -50,12 +50,12 @@ class StemPressReaction @Inject constructor(
|
||||
}
|
||||
|
||||
private suspend fun executeAction(action: StemAction) = when (action) {
|
||||
StemAction.NONE -> Unit
|
||||
StemAction.NONE, StemAction.NO_ACTION -> Unit
|
||||
StemAction.PLAY_PAUSE -> mediaControl.sendPlayPause()
|
||||
StemAction.NEXT_TRACK -> mediaControl.sendKey(KeyEvent.KEYCODE_MEDIA_NEXT)
|
||||
StemAction.PREVIOUS_TRACK -> mediaControl.sendKey(KeyEvent.KEYCODE_MEDIA_PREVIOUS)
|
||||
StemAction.VOLUME_UP -> mediaControl.sendKey(KeyEvent.KEYCODE_VOLUME_UP)
|
||||
StemAction.VOLUME_DOWN -> mediaControl.sendKey(KeyEvent.KEYCODE_VOLUME_DOWN)
|
||||
StemAction.VOLUME_UP -> mediaControl.adjustVolumeUp()
|
||||
StemAction.VOLUME_DOWN -> mediaControl.adjustVolumeDown()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -5,6 +5,7 @@ import kotlinx.serialization.Serializable
|
||||
@Serializable
|
||||
enum class StemAction {
|
||||
NONE,
|
||||
NO_ACTION,
|
||||
PLAY_PAUSE,
|
||||
NEXT_TRACK,
|
||||
PREVIOUS_TRACK,
|
||||
|
||||
@@ -3,6 +3,7 @@ package eu.darken.capod.reaction.core.stem
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import eu.darken.capod.common.datastore.createValue
|
||||
@@ -29,4 +30,8 @@ class StemActionSettings @Inject constructor(
|
||||
val rightDouble = dataStore.createValue("stem.right.double", StemAction.NONE, json, onErrorFallbackToDefault = true)
|
||||
val rightTriple = dataStore.createValue("stem.right.triple", StemAction.NONE, json, onErrorFallbackToDefault = true)
|
||||
val rightLong = dataStore.createValue("stem.right.long", StemAction.NONE, json, onErrorFallbackToDefault = true)
|
||||
|
||||
suspend fun resetAll() {
|
||||
context.dataStore.edit { it.clear() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -510,11 +510,14 @@
|
||||
<string name="stem_actions_long_press">Long Press</string>
|
||||
<string name="stem_actions_left">Left</string>
|
||||
<string name="stem_actions_right">Right</string>
|
||||
<string name="stem_action_none">None</string>
|
||||
<string name="stem_action_none">Default</string>
|
||||
<string name="stem_action_no_action">No Action</string>
|
||||
<string name="stem_action_play_pause">Play/Pause</string>
|
||||
<string name="stem_action_next_track">Next Track</string>
|
||||
<string name="stem_action_previous_track">Previous Track</string>
|
||||
<string name="stem_action_volume_up">Volume Up</string>
|
||||
<string name="stem_action_volume_down">Volume Down</string>
|
||||
<string name="stem_actions_reset_label">Reset to defaults</string>
|
||||
<string name="stem_actions_reset_confirm_message">Reset all stem actions to defaults?</string>
|
||||
|
||||
</resources>
|
||||
Reference in New Issue
Block a user