refactor: improve accessibility title for player

This commit is contained in:
Antoine Jaury
2026-10-12 14:13:27 +02:00
parent 1a23703911
commit 8dc1e8e71e
5 changed files with 62 additions and 34 deletions
@@ -0,0 +1,23 @@
package bzh.ajaury.chombev
import androidx.compose.runtime.Composable
import bzh.ajaury.chombev.core.model.Phrase
import bzh.ajaury.chombev.core.model.Record
import bzh.ajaury.chombev.resources.generated.resources.Res
import bzh.ajaury.chombev.resources.generated.resources.records_chapter_number
import org.jetbrains.compose.resources.stringResource
@Composable
internal fun rowDescription(record: Record): String = rowDescription(index = record.index, title = record.title)
@Composable
internal fun rowDescription(
index: Int,
title: Phrase,
): String {
val chapterNumber = stringResource(Res.string.records_chapter_number, index)
val rowDescription =
listOfNotNull(chapterNumber, title.translation)
.joinToString(separator = ", ")
return rowDescription
}
@@ -40,6 +40,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import bzh.ajaury.chombev.core.model.Phrase import bzh.ajaury.chombev.core.model.Phrase
import bzh.ajaury.chombev.core.model.Record
import bzh.ajaury.chombev.player.model.PlaybackState import bzh.ajaury.chombev.player.model.PlaybackState
import bzh.ajaury.chombev.player.model.PlaybackTiming import bzh.ajaury.chombev.player.model.PlaybackTiming
import bzh.ajaury.chombev.player.model.PlayerState import bzh.ajaury.chombev.player.model.PlayerState
@@ -51,6 +52,7 @@ import bzh.ajaury.chombev.resources.generated.resources.player_back
import bzh.ajaury.chombev.resources.generated.resources.player_back_action_label import bzh.ajaury.chombev.resources.generated.resources.player_back_action_label
import bzh.ajaury.chombev.resources.generated.resources.settings_open_action_description import bzh.ajaury.chombev.resources.generated.resources.settings_open_action_description
import bzh.ajaury.chombev.resources.generated.resources.subtitle_preferences_action import bzh.ajaury.chombev.resources.generated.resources.subtitle_preferences_action
import bzh.ajaury.chombev.rowDescription
import bzh.ajaury.chombev.subtitle.model.SubtitleLine import bzh.ajaury.chombev.subtitle.model.SubtitleLine
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
import org.koin.compose.viewmodel.koinViewModel import org.koin.compose.viewmodel.koinViewModel
@@ -204,35 +206,38 @@ private fun PlayerTopBar(
// Animate the title in as it loads, and between records: the record data isn't there on // Animate the title in as it loads, and between records: the record data isn't there on
// the first frame, so fade + rise it into place once uiState carries it. // the first frame, so fade + rise it into place once uiState carries it.
AnimatedContent( AnimatedContent(
targetState = uiState.recordEmoji to uiState.recordTitle, targetState = uiState.record,
transitionSpec = { transitionSpec = {
fadeIn(tween()) togetherWith fadeIn(tween()) togetherWith
fadeOut(tween()) fadeOut(tween())
}, },
label = "PlayerTitle", label = "PlayerTitle",
) { (emoji, title) -> ) { record ->
// Same description as the record's row in the list; none until the record is loaded.
val description =
record?.index?.let { rowDescription(index = it, title = record.title) }
Row( Row(
modifier = Modifier modifier = Modifier
.traversalOrder(PlayerTraversalOrder.TITLE) .traversalOrder(PlayerTraversalOrder.TITLE)
.clearAndSetSemantics { .clearAndSetSemantics {
contentDescription = title.translation ?: title.transcription description?.let { contentDescription = it }
}, },
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp),
) { ) {
Text( Text(
text = emoji, text = record?.emoji.orEmpty(),
style = MaterialTheme.typography.headlineSmall, style = MaterialTheme.typography.headlineSmall,
) )
Column { Column {
Text( Text(
text = title.transcription, text = record?.title?.transcription.orEmpty(),
style = MaterialTheme.typography.headlineSmall, style = MaterialTheme.typography.headlineSmall,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
) )
title.translation?.let { translation -> record?.title?.translation?.let { translation ->
Text( Text(
text = translation, text = translation,
style = MaterialTheme.typography.titleSmall, style = MaterialTheme.typography.titleSmall,
@@ -317,17 +322,25 @@ private val previewSubtitleLines =
), ),
) )
private val fakeRecord = Record(
id = 1,
index = 1,
emoji = "🙏",
title = Phrase(
transcription = "Demat",
translation = "Bonjour",
),
audioResourcePath = "",
subtitleResourcePath = "",
)
@Preview @Preview
@Composable @Composable
private fun PlayerScreenPlayingPreview() { private fun PlayerScreenPlayingPreview() {
MaterialTheme { MaterialTheme {
PlayerScreen( PlayerScreen(
uiState = PlayerUiState( uiState = PlayerUiState(
recordEmoji = "🙏", fakeRecord,
recordTitle = Phrase(
transcription = "Demat",
translation = "Bonjour",
),
playerState = PlayerState( playerState = PlayerState(
playbackState = PlaybackState.PLAYING, playbackState = PlaybackState.PLAYING,
playbackTiming = PlaybackTiming(position = 1.2.seconds, duration = 5.seconds), playbackTiming = PlaybackTiming(position = 1.2.seconds, duration = 5.seconds),
@@ -348,14 +361,13 @@ private fun PlayerScreenPausedPreview() {
MaterialTheme { MaterialTheme {
PlayerScreen( PlayerScreen(
uiState = PlayerUiState( uiState = PlayerUiState(
recordEmoji = "🙏", record = fakeRecord,
recordTitle = Phrase(
transcription = "Demat",
translation = "Bonjour",
),
playerState = PlayerState( playerState = PlayerState(
playbackState = PlaybackState.PAUSED, playbackState = PlaybackState.PAUSED,
playbackTiming = PlaybackTiming(position = 1.2.seconds, duration = 5.seconds), playbackTiming = PlaybackTiming(
position = 1.2.seconds,
duration = 5.seconds,
),
subtitleLines = previewSubtitleLines, subtitleLines = previewSubtitleLines,
currentSubtitleIndex = 1, currentSubtitleIndex = 1,
), ),
@@ -373,11 +385,7 @@ private fun PlayerScreenEndedPreview() {
MaterialTheme { MaterialTheme {
PlayerScreen( PlayerScreen(
uiState = PlayerUiState( uiState = PlayerUiState(
recordEmoji = "🙏", record = fakeRecord,
recordTitle = Phrase(
transcription = "Demat",
translation = "Bonjour",
),
playerState = PlayerState( playerState = PlayerState(
playbackState = PlaybackState.ENDED, playbackState = PlaybackState.ENDED,
playbackTiming = PlaybackTiming(position = 1.2.seconds, duration = 5.seconds), playbackTiming = PlaybackTiming(position = 1.2.seconds, duration = 5.seconds),
@@ -1,6 +1,6 @@
package bzh.ajaury.chombev.player.ui.viewmodel package bzh.ajaury.chombev.player.ui.viewmodel
import bzh.ajaury.chombev.core.model.Phrase import bzh.ajaury.chombev.core.model.Record
import bzh.ajaury.chombev.player.model.PlayerState import bzh.ajaury.chombev.player.model.PlayerState
import bzh.ajaury.chombev.preferences.model.PlaybackPreferences import bzh.ajaury.chombev.preferences.model.PlaybackPreferences
import bzh.ajaury.chombev.preferences.model.SubtitlePreferences import bzh.ajaury.chombev.preferences.model.SubtitlePreferences
@@ -8,8 +8,7 @@ import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
data class PlayerUiState( data class PlayerUiState(
val recordEmoji: String = "", val record: Record? = null,
val recordTitle: Phrase = Phrase(transcription = ""),
val playerState: PlayerState = PlayerState(), val playerState: PlayerState = PlayerState(),
val subtitlePreferences: SubtitlePreferences = SubtitlePreferences(), val subtitlePreferences: SubtitlePreferences = SubtitlePreferences(),
val playbackSpeed: Float = PlaybackPreferences.DEFAULT_SPEED, val playbackSpeed: Float = PlaybackPreferences.DEFAULT_SPEED,
@@ -20,7 +19,10 @@ data class PlayerUiState(
get() = playerState.playbackTiming.position > 2.seconds get() = playerState.playbackTiming.position > 2.seconds
val canGoToNextSentence: Boolean val canGoToNextSentence: Boolean
get() = (playerState.currentSubtitleIndex ?: Int.MAX_VALUE) < playerState.subtitleLines.lastIndex get() = (
playerState.currentSubtitleIndex
?: Int.MAX_VALUE
) < playerState.subtitleLines.lastIndex
/** True when any playback setting differs from its default, used to highlight the button. */ /** True when any playback setting differs from its default, used to highlight the button. */
val isPlaybackModified: Boolean val isPlaybackModified: Boolean
@@ -4,7 +4,6 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import bzh.ajaury.chombev.core.accessibility.domain.AccessibilityRepository import bzh.ajaury.chombev.core.accessibility.domain.AccessibilityRepository
import bzh.ajaury.chombev.core.logging.domain.Logger import bzh.ajaury.chombev.core.logging.domain.Logger
import bzh.ajaury.chombev.core.model.Phrase
import bzh.ajaury.chombev.core.model.Record import bzh.ajaury.chombev.core.model.Record
import bzh.ajaury.chombev.player.domain.AudioSessionManager import bzh.ajaury.chombev.player.domain.AudioSessionManager
import bzh.ajaury.chombev.player.domain.PlaybackRepository import bzh.ajaury.chombev.player.domain.PlaybackRepository
@@ -42,8 +41,7 @@ class PlayerViewModel(
preferencesRepository.playbackPreferences, preferencesRepository.playbackPreferences,
) { record, playerState, subtitlePreferences, playbackPreferences -> ) { record, playerState, subtitlePreferences, playbackPreferences ->
PlayerUiState( PlayerUiState(
recordEmoji = record?.emoji ?: "", record = record,
recordTitle = record?.title ?: Phrase(transcription = ""),
playerState = playerState, playerState = playerState,
subtitlePreferences = subtitlePreferences, subtitlePreferences = subtitlePreferences,
playbackSpeed = playbackPreferences.speed, playbackSpeed = playbackPreferences.speed,
@@ -21,7 +21,7 @@ import bzh.ajaury.chombev.core.model.Phrase
import bzh.ajaury.chombev.core.model.Record import bzh.ajaury.chombev.core.model.Record
import bzh.ajaury.chombev.resources.generated.resources.Res import bzh.ajaury.chombev.resources.generated.resources.Res
import bzh.ajaury.chombev.resources.generated.resources.records_chapter_action_description import bzh.ajaury.chombev.resources.generated.resources.records_chapter_action_description
import bzh.ajaury.chombev.resources.generated.resources.records_chapter_number import bzh.ajaury.chombev.rowDescription
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
@Composable @Composable
@@ -29,10 +29,7 @@ fun RecordView(
record: Record, record: Record,
onClick: () -> Unit = {}, onClick: () -> Unit = {},
) { ) {
val chapterNumber = stringResource(Res.string.records_chapter_number, record.index) val rowDescription = rowDescription(record)
val rowDescription =
listOfNotNull(chapterNumber, record.title.transcription, record.title.translation)
.joinToString(separator = ", ")
Row( Row(
modifier = modifier =