settings: snap the preamp to its grid and drop a duplicate header

Review follow-ups.

- Remove the second "Playback" section header the rows were added
  under; the section already had one, so the screen rendered it twice.
- Snap the preamp to the 0.5 dB grid in the dialog. Material's slider
  snaps in pixel space and converts back, so the value it reports is near
  the configured grid but not on it: -1.0 dB arrives as -0.99999905, was
  displayed verbatim and was stored that way until the next drag. The
  step count is now derived from the range rather than hardcoded.
- Format the decibel value with the resource locale so translated
  layouts get their own decimal separator.
- The controls move to `ReplayGainControls.kt`, since the file now holds
  the grid as well as the labels, and the snap is covered by a test that
  reproduces the off-grid value without it.
This commit is contained in:
austinried
2026-10-16 13:07:11 +00:00
parent 229d6de4c6
commit 0b4e1d18bc
4 changed files with 58 additions and 29 deletions
@@ -0,0 +1,37 @@
package com.subtracks.ui.settings
import android.content.res.Resources
import com.subtracks.R
import com.subtracks.data.prefs.MAX_REPLAY_GAIN_PREAMP_DB
import com.subtracks.data.prefs.MIN_REPLAY_GAIN_PREAMP_DB
import com.subtracks.data.prefs.ReplayGainMode
import kotlin.math.roundToInt
internal const val REPLAY_GAIN_PREAMP_STEP_DB = 0.5f
internal val REPLAY_GAIN_PREAMP_STEPS =
((MAX_REPLAY_GAIN_PREAMP_DB - MIN_REPLAY_GAIN_PREAMP_DB) / REPLAY_GAIN_PREAMP_STEP_DB).toInt() - 1
// Material's slider snaps in pixel space and converts back, so the value it reports is near, but not
// on, the grid it was configured with (for example -0.99999905 for -1.0). Snap before showing or
// storing it.
internal fun snapReplayGainPreampDb(value: Float): Float = (value / REPLAY_GAIN_PREAMP_STEP_DB).roundToInt() * REPLAY_GAIN_PREAMP_STEP_DB
internal fun replayGainModeLabel(
resources: Resources,
mode: ReplayGainMode,
): String =
when (mode) {
ReplayGainMode.Off -> resources.getString(R.string.settings_replay_gain_mode_off)
ReplayGainMode.Track -> resources.getString(R.string.settings_replay_gain_mode_track)
ReplayGainMode.Album -> resources.getString(R.string.settings_replay_gain_mode_album)
}
internal fun replayGainPreampLabel(
resources: Resources,
decibels: Float,
): String {
val sign = if (decibels > 0f) "+" else ""
val magnitude = String.format(resources.configuration.locales[0], "%.1f", decibels)
return resources.getString(R.string.settings_replay_gain_preamp_value, "$sign$magnitude")
}
@@ -1,23 +0,0 @@
package com.subtracks.ui.settings
import android.content.res.Resources
import com.subtracks.R
import com.subtracks.data.prefs.ReplayGainMode
internal fun replayGainModeLabel(
resources: Resources,
mode: ReplayGainMode,
): String =
when (mode) {
ReplayGainMode.Off -> resources.getString(R.string.settings_replay_gain_mode_off)
ReplayGainMode.Track -> resources.getString(R.string.settings_replay_gain_mode_track)
ReplayGainMode.Album -> resources.getString(R.string.settings_replay_gain_mode_album)
}
internal fun replayGainPreampLabel(
resources: Resources,
decibels: Float,
): String {
val sign = if (decibels > 0f) "+" else ""
return resources.getString(R.string.settings_replay_gain_preamp_value, "$sign$decibels")
}
@@ -361,7 +361,6 @@ fun SettingsScreen(
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
)
}
item { SectionHeader(stringResource(R.string.settings_music_name)) }
item {
ListItem(
headlineContent = { Text(stringResource(R.string.settings_replay_gain_title)) },
@@ -729,8 +728,6 @@ private fun SyncModeDialog(
)
}
private const val REPLAY_GAIN_PREAMP_STEPS = 59
@Composable
private fun ReplayGainModeDialog(
selected: ReplayGainMode,
@@ -771,7 +768,7 @@ private fun ReplayGainPreampDialog(
onDismiss: () -> Unit,
) {
val resources = LocalResources.current
var draft by remember(selected) { mutableFloatStateOf(selected) }
var draft by remember(selected) { mutableFloatStateOf(snapReplayGainPreampDb(selected)) }
AlertDialog(
onDismissRequest = onDismiss,
title = { Text(stringResource(R.string.settings_replay_gain_preamp_title)) },
@@ -784,7 +781,7 @@ private fun ReplayGainPreampDialog(
)
Slider(
value = draft,
onValueChange = { draft = it },
onValueChange = { draft = snapReplayGainPreampDb(it) },
valueRange = MIN_REPLAY_GAIN_PREAMP_DB..MAX_REPLAY_GAIN_PREAMP_DB,
steps = REPLAY_GAIN_PREAMP_STEPS,
)
@@ -3,13 +3,15 @@ package com.subtracks.ui.settings
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.subtracks.data.prefs.MAX_REPLAY_GAIN_PREAMP_DB
import com.subtracks.data.prefs.MIN_REPLAY_GAIN_PREAMP_DB
import com.subtracks.data.prefs.ReplayGainMode
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class ReplayGainLabelsTest {
class ReplayGainControlsTest {
private val resources = ApplicationProvider.getApplicationContext<Context>().resources
@Test
@@ -25,4 +27,20 @@ class ReplayGainLabelsTest {
assertEquals("-2.5 dB", replayGainPreampLabel(resources, -2.5f))
assertEquals("0.0 dB", replayGainPreampLabel(resources, 0f))
}
@Test
fun theSliderHasOneStepPerHalfDecibel() {
assertEquals(60, ((MAX_REPLAY_GAIN_PREAMP_DB - MIN_REPLAY_GAIN_PREAMP_DB) / REPLAY_GAIN_PREAMP_STEP_DB).toInt())
assertEquals(59, REPLAY_GAIN_PREAMP_STEPS)
}
@Test
fun thePreampSnapsBackOntoTheHalfDecibelGrid() {
assertEquals(0f, snapReplayGainPreampDb(-0f), 0f)
assertEquals(-1f, snapReplayGainPreampDb(-0.99999905f), 0f)
assertEquals(-3f, snapReplayGainPreampDb(-3.000001f), 0f)
assertEquals(14.5f, snapReplayGainPreampDb(14.6f), 0f)
assertEquals(MAX_REPLAY_GAIN_PREAMP_DB, snapReplayGainPreampDb(14.9f), 0f)
assertEquals(MIN_REPLAY_GAIN_PREAMP_DB, snapReplayGainPreampDb(-14.9f), 0f)
}
}