refactor: improve accessibility for audio settings

This commit is contained in:
Antoine Jaury
2026-10-12 15:02:41 +02:00
parent 82a2ef3bdb
commit 5539bf8b0d
3 changed files with 60 additions and 1 deletions
@@ -24,9 +24,14 @@
<string name="subtitle_marked_letters_description">Afficher les consonnes muettes et les liaisons</string> <string name="subtitle_marked_letters_description">Afficher les consonnes muettes et les liaisons</string>
<string name="playback_speed_title">Vitesse de lecture</string> <string name="playback_speed_title">Vitesse de lecture</string>
<string name="playback_speed_value">%1$d %</string>
<string name="playback_settings_action">Réglages de lecture</string> <string name="playback_settings_action">Réglages de lecture</string>
<string name="break_between_phrases_title">Pause entre les phrases</string> <string name="break_between_phrases_title">Pause entre les phrases</string>
<string name="break_between_phrases_value">%1$d s</string> <string name="break_between_phrases_value">%1$d s</string>
<plurals name="break_between_phrases_value_description">
<item quantity="one">%1$d seconde</item>
<item quantity="other">%1$d secondes</item>
</plurals>
<string name="auto_pause_title">Pause automatique</string> <string name="auto_pause_title">Pause automatique</string>
<string name="auto_pause_description">Mettre en pause après chaque phrase</string> <string name="auto_pause_description">Mettre en pause après chaque phrase</string>
@@ -22,7 +22,10 @@ import bzh.ajaury.chombev.resources.generated.resources.auto_pause_description
import bzh.ajaury.chombev.resources.generated.resources.auto_pause_title import bzh.ajaury.chombev.resources.generated.resources.auto_pause_title
import bzh.ajaury.chombev.resources.generated.resources.break_between_phrases_title import bzh.ajaury.chombev.resources.generated.resources.break_between_phrases_title
import bzh.ajaury.chombev.resources.generated.resources.break_between_phrases_value import bzh.ajaury.chombev.resources.generated.resources.break_between_phrases_value
import bzh.ajaury.chombev.resources.generated.resources.break_between_phrases_value_description
import bzh.ajaury.chombev.resources.generated.resources.playback_speed_title import bzh.ajaury.chombev.resources.generated.resources.playback_speed_title
import bzh.ajaury.chombev.resources.generated.resources.playback_speed_value
import org.jetbrains.compose.resources.pluralStringResource
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
import kotlin.math.roundToInt import kotlin.math.roundToInt
@@ -85,6 +88,8 @@ private fun PlaybackSpeedContent(
LabeledSlider( LabeledSlider(
title = stringResource(Res.string.playback_speed_title), title = stringResource(Res.string.playback_speed_title),
valueLabel = formatPlaybackSpeed(speed), valueLabel = formatPlaybackSpeed(speed),
// "x0.7" reads poorly aloud: screen readers get "70 %" instead.
valueDescription = stringResource(Res.string.playback_speed_value, (speed * 100).roundToInt()),
value = speed, value = speed,
valueRange = PlaybackPreferences.MIN_SPEED..PlaybackPreferences.MAX_SPEED, valueRange = PlaybackPreferences.MIN_SPEED..PlaybackPreferences.MAX_SPEED,
steps = speedSteps, steps = speedSteps,
@@ -102,6 +107,12 @@ private fun PlaybackSpeedContent(
Res.string.break_between_phrases_value, Res.string.break_between_phrases_value,
breakSeconds.roundToInt().toString(), breakSeconds.roundToInt().toString(),
), ),
// "2 s" may be spelled out letter by letter: screen readers get "2 secondes" instead.
valueDescription = pluralStringResource(
Res.plurals.break_between_phrases_value_description,
breakSeconds.roundToInt(),
breakSeconds.roundToInt(),
),
value = breakSeconds, value = breakSeconds,
valueRange = 0f..maxBreakSeconds.toFloat(), valueRange = 0f..maxBreakSeconds.toFloat(),
steps = breakSteps, steps = breakSteps,
@@ -10,8 +10,16 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.ProgressBarRangeInfo
import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.disabled
import androidx.compose.ui.semantics.progressBarRangeInfo
import androidx.compose.ui.semantics.setProgress
import androidx.compose.ui.semantics.stateDescription
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import kotlin.math.roundToInt
@Composable @Composable
fun LabeledSlider( fun LabeledSlider(
@@ -20,13 +28,37 @@ fun LabeledSlider(
value: Float, value: Float,
valueRange: ClosedFloatingPointRange<Float>, valueRange: ClosedFloatingPointRange<Float>,
steps: Int, steps: Int,
valueDescription: String = valueLabel,
onValueChange: (Float) -> Unit = {}, onValueChange: (Float) -> Unit = {},
enabled: Boolean = true, enabled: Boolean = true,
) { ) {
// Dim the labels to match the Slider's disabled appearance. // Dim the labels to match the Slider's disabled appearance.
val contentAlpha = if (enabled) 1f else 0.38f val contentAlpha = if (enabled) 1f else 0.38f
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { Column(
// The whole block (title, value and slider) is a single adjustable element for screen
// readers, named by the title: swiping up or down moves one step and reads the new value.
modifier = Modifier.clearAndSetSemantics {
contentDescription = title
stateDescription = valueDescription
progressBarRangeInfo = ProgressBarRangeInfo(
current = value,
range = valueRange,
steps = steps,
)
if (enabled) {
setProgress { target ->
val newValue = target.snapToSteps(valueRange, steps)
if (newValue == value) return@setProgress false
onValueChange(newValue)
true
}
} else {
disabled()
}
},
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
@@ -53,6 +85,17 @@ fun LabeledSlider(
} }
} }
private fun Float.snapToSteps(
range: ClosedFloatingPointRange<Float>,
steps: Int,
): Float {
val clamped = coerceIn(range)
if (steps <= 0) return clamped
val stepSize = (range.endInclusive - range.start) / (steps + 1)
val stepIndex = ((clamped - range.start) / stepSize).roundToInt()
return (range.start + stepIndex * stepSize).coerceIn(range)
}
@Preview @Preview
@Composable @Composable
private fun LabeledSliderPreview() { private fun LabeledSliderPreview() {