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="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>
|
||||||
|
|
||||||
|
|||||||
+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.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,
|
||||||
|
|||||||
+44
-1
@@ -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() {
|
||||||
|
|||||||
Reference in New Issue
Block a user