From 5539bf8b0d4d9332a899988c5c4d9c1a40b91844 Mon Sep 17 00:00:00 2001 From: Antoine Jaury Date: Mon, 12 Oct 2026 15:00:52 +0200 Subject: [PATCH] refactor: improve accessibility for audio settings --- .../composeResources/values/strings.xml | 5 +++ .../player/ui/PlaybackSpeedBottomSheet.kt | 11 +++++ .../player/ui/components/LabeledSlider.kt | 45 ++++++++++++++++++- 3 files changed, 60 insertions(+), 1 deletion(-) diff --git a/data/resources/src/commonMain/composeResources/values/strings.xml b/data/resources/src/commonMain/composeResources/values/strings.xml index 42c5548..5c648fd 100644 --- a/data/resources/src/commonMain/composeResources/values/strings.xml +++ b/data/resources/src/commonMain/composeResources/values/strings.xml @@ -24,9 +24,14 @@ Afficher les consonnes muettes et les liaisons Vitesse de lecture + %1$d % Réglages de lecture Pause entre les phrases %1$d s + + %1$d seconde + %1$d secondes + Pause automatique Mettre en pause après chaque phrase diff --git a/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/player/ui/PlaybackSpeedBottomSheet.kt b/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/player/ui/PlaybackSpeedBottomSheet.kt index 2f08c9c..f5fbef2 100644 --- a/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/player/ui/PlaybackSpeedBottomSheet.kt +++ b/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/player/ui/PlaybackSpeedBottomSheet.kt @@ -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.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_description 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 kotlin.math.roundToInt @@ -85,6 +88,8 @@ private fun PlaybackSpeedContent( LabeledSlider( title = stringResource(Res.string.playback_speed_title), 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, valueRange = PlaybackPreferences.MIN_SPEED..PlaybackPreferences.MAX_SPEED, steps = speedSteps, @@ -102,6 +107,12 @@ private fun PlaybackSpeedContent( Res.string.break_between_phrases_value, 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, valueRange = 0f..maxBreakSeconds.toFloat(), steps = breakSteps, diff --git a/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/player/ui/components/LabeledSlider.kt b/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/player/ui/components/LabeledSlider.kt index 0a8bffd..873d6e7 100644 --- a/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/player/ui/components/LabeledSlider.kt +++ b/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/player/ui/components/LabeledSlider.kt @@ -10,8 +10,16 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment 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.unit.dp +import kotlin.math.roundToInt @Composable fun LabeledSlider( @@ -20,13 +28,37 @@ fun LabeledSlider( value: Float, valueRange: ClosedFloatingPointRange, steps: Int, + valueDescription: String = valueLabel, onValueChange: (Float) -> Unit = {}, enabled: Boolean = true, ) { // Dim the labels to match the Slider's disabled appearance. 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( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, @@ -53,6 +85,17 @@ fun LabeledSlider( } } +private fun Float.snapToSteps( + range: ClosedFloatingPointRange, + 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 @Composable private fun LabeledSliderPreview() {