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.lifecycle.compose.collectAsStateWithLifecycle
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.PlaybackTiming
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.settings_open_action_description
import bzh.ajaury.chombev.resources.generated.resources.subtitle_preferences_action
import bzh.ajaury.chombev.rowDescription
import bzh.ajaury.chombev.subtitle.model.SubtitleLine
import org.jetbrains.compose.resources.stringResource
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
// the first frame, so fade + rise it into place once uiState carries it.
AnimatedContent(
targetState = uiState.recordEmoji to uiState.recordTitle,
targetState = uiState.record,
transitionSpec = {
fadeIn(tween()) togetherWith
fadeOut(tween())
},
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(
modifier = Modifier
.traversalOrder(PlayerTraversalOrder.TITLE)
.clearAndSetSemantics {
contentDescription = title.translation ?: title.transcription
description?.let { contentDescription = it }
},
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
Text(
text = emoji,
text = record?.emoji.orEmpty(),
style = MaterialTheme.typography.headlineSmall,
)
Column {
Text(
text = title.transcription,
text = record?.title?.transcription.orEmpty(),
style = MaterialTheme.typography.headlineSmall,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
title.translation?.let { translation ->
record?.title?.translation?.let { translation ->
Text(
text = translation,
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
@Composable
private fun PlayerScreenPlayingPreview() {
MaterialTheme {
PlayerScreen(
uiState = PlayerUiState(
recordEmoji = "🙏",
recordTitle = Phrase(
transcription = "Demat",
translation = "Bonjour",
),
fakeRecord,
playerState = PlayerState(
playbackState = PlaybackState.PLAYING,
playbackTiming = PlaybackTiming(position = 1.2.seconds, duration = 5.seconds),
@@ -348,14 +361,13 @@ private fun PlayerScreenPausedPreview() {
MaterialTheme {
PlayerScreen(
uiState = PlayerUiState(
recordEmoji = "🙏",
recordTitle = Phrase(
transcription = "Demat",
translation = "Bonjour",
),
record = fakeRecord,
playerState = PlayerState(
playbackState = PlaybackState.PAUSED,
playbackTiming = PlaybackTiming(position = 1.2.seconds, duration = 5.seconds),
playbackTiming = PlaybackTiming(
position = 1.2.seconds,
duration = 5.seconds,
),
subtitleLines = previewSubtitleLines,
currentSubtitleIndex = 1,
),
@@ -373,11 +385,7 @@ private fun PlayerScreenEndedPreview() {
MaterialTheme {
PlayerScreen(
uiState = PlayerUiState(
recordEmoji = "🙏",
recordTitle = Phrase(
transcription = "Demat",
translation = "Bonjour",
),
record = fakeRecord,
playerState = PlayerState(
playbackState = PlaybackState.ENDED,
playbackTiming = PlaybackTiming(position = 1.2.seconds, duration = 5.seconds),
@@ -1,6 +1,6 @@
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.preferences.model.PlaybackPreferences
import bzh.ajaury.chombev.preferences.model.SubtitlePreferences
@@ -8,8 +8,7 @@ import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
data class PlayerUiState(
val recordEmoji: String = "",
val recordTitle: Phrase = Phrase(transcription = ""),
val record: Record? = null,
val playerState: PlayerState = PlayerState(),
val subtitlePreferences: SubtitlePreferences = SubtitlePreferences(),
val playbackSpeed: Float = PlaybackPreferences.DEFAULT_SPEED,
@@ -20,7 +19,10 @@ data class PlayerUiState(
get() = playerState.playbackTiming.position > 2.seconds
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. */
val isPlaybackModified: Boolean
@@ -4,7 +4,6 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import bzh.ajaury.chombev.core.accessibility.domain.AccessibilityRepository
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.player.domain.AudioSessionManager
import bzh.ajaury.chombev.player.domain.PlaybackRepository
@@ -42,8 +41,7 @@ class PlayerViewModel(
preferencesRepository.playbackPreferences,
) { record, playerState, subtitlePreferences, playbackPreferences ->
PlayerUiState(
recordEmoji = record?.emoji ?: "",
recordTitle = record?.title ?: Phrase(transcription = ""),
record = record,
playerState = playerState,
subtitlePreferences = subtitlePreferences,
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.resources.generated.resources.Res
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
@Composable
@@ -29,10 +29,7 @@ fun RecordView(
record: Record,
onClick: () -> Unit = {},
) {
val chapterNumber = stringResource(Res.string.records_chapter_number, record.index)
val rowDescription =
listOfNotNull(chapterNumber, record.title.transcription, record.title.translation)
.joinToString(separator = ", ")
val rowDescription = rowDescription(record)
Row(
modifier =