refactor: improve accessibility for audio settings
This commit is contained in:
@@ -24,9 +24,14 @@
|
||||
<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_value">%1$d %</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_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_description">Mettre en pause après chaque phrase</string>
|
||||
|
||||
|
||||
+11
@@ -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,
|
||||
|
||||
+44
-1
@@ -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<Float>,
|
||||
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<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
|
||||
@Composable
|
||||
private fun LabeledSliderPreview() {
|
||||
|
||||
Reference in New Issue
Block a user